@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
|
@@ -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
|
}
|
|
@@ -186,12 +186,6 @@ export async function buildCliServicePosture(
|
|
|
186
186
|
if (serverBackedEnabled && !config.enabled) {
|
|
187
187
|
issues.push('Connected-service settings are present, but Agent service ownership is disabled by design.');
|
|
188
188
|
}
|
|
189
|
-
if (config.enabled && !config.autostart) {
|
|
190
|
-
issues.push('Connected-service autostart is off.');
|
|
191
|
-
}
|
|
192
|
-
if (config.enabled && !config.restartOnFailure) {
|
|
193
|
-
issues.push('Connected-service restart-on-failure is off.');
|
|
194
|
-
}
|
|
195
189
|
for (const endpoint of endpoints) {
|
|
196
190
|
if (endpoint.enabled && options.probe && endpoint.reachable === false) {
|
|
197
191
|
issues.push(`${endpoint.label} is enabled but not reachable on ${endpoint.binding.host}:${endpoint.binding.port}.`);
|
|
@@ -233,8 +227,7 @@ export function formatCliServicePosture(posture: CliServicePosture, json = false
|
|
|
233
227
|
' ownership: managed outside goodvibes-agent',
|
|
234
228
|
' Agent owns lifecycle: no',
|
|
235
229
|
` external host config present: ${yesNo(posture.config.enabled)}`,
|
|
236
|
-
|
|
237
|
-
` external host restart config: ${yesNo(posture.config.restartOnFailure)}`,
|
|
230
|
+
' external host lifecycle config: ignored by Agent',
|
|
238
231
|
` legacy host switch present: ${yesNo(posture.config.daemonEnabled)}`,
|
|
239
232
|
` log: ${posture.log.path ?? 'n/a'} (${posture.log.exists ? 'present' : 'missing'})`,
|
|
240
233
|
...(posture.log.readError ? [` log read error: ${posture.log.readError}`] : []),
|
package/src/cli/status.ts
CHANGED
|
@@ -99,8 +99,6 @@ function bindLine(label: string, enabled: unknown, binding: { readonly hostMode:
|
|
|
99
99
|
export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliDoctorFinding[] {
|
|
100
100
|
const config = options.configManager;
|
|
101
101
|
const serviceEnabled = config.get('service.enabled') === true;
|
|
102
|
-
const serviceAutostart = config.get('service.autostart') === true;
|
|
103
|
-
const restartOnFailure = config.get('service.restartOnFailure') === true;
|
|
104
102
|
const daemonEnabled = config.get('danger.daemon') === true;
|
|
105
103
|
const listenerEnabled = config.get('danger.httpListener') === true;
|
|
106
104
|
const webEnabled = config.get('web.enabled') === true;
|
|
@@ -180,30 +178,6 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
180
178
|
});
|
|
181
179
|
}
|
|
182
180
|
|
|
183
|
-
if (serviceEnabled && !serviceAutostart) {
|
|
184
|
-
findings.push({
|
|
185
|
-
id: 'runtime-autostart-disabled',
|
|
186
|
-
area: 'runtime',
|
|
187
|
-
severity: 'warning',
|
|
188
|
-
summary: 'Connected-service autostart is off.',
|
|
189
|
-
cause: 'service.enabled is true and service.autostart is false.',
|
|
190
|
-
impact: 'Connected GoodVibes services may not be available after login or reboot even though host-managed startup is selected.',
|
|
191
|
-
action: 'Configure autostart from GoodVibes TUI or the owning host; Agent will not mutate this setting.',
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (serviceEnabled && !restartOnFailure) {
|
|
196
|
-
findings.push({
|
|
197
|
-
id: 'runtime-restart-disabled',
|
|
198
|
-
area: 'runtime',
|
|
199
|
-
severity: 'warning',
|
|
200
|
-
summary: 'Connected-service restart-on-failure is off.',
|
|
201
|
-
cause: 'service.enabled is true and service.restartOnFailure is false.',
|
|
202
|
-
impact: 'A crashed connected service or listener may stay down until manually restarted.',
|
|
203
|
-
action: 'Configure restart-on-failure from GoodVibes TUI or the owning host; Agent will not mutate this setting.',
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
181
|
if (options.service) {
|
|
208
182
|
for (const issue of options.service.issues) {
|
|
209
183
|
if (findings.some((finding) => finding.summary === issue)) continue;
|
|
@@ -342,8 +316,6 @@ export function renderCliStatus(options: CliStatusOptions): string {
|
|
|
342
316
|
const config = options.configManager;
|
|
343
317
|
const snapshot = buildCliStatusSnapshot(options);
|
|
344
318
|
const serviceEnabled = snapshot.runtimeConnection.enabled;
|
|
345
|
-
const serviceAutostart = snapshot.runtimeConnection.autostart;
|
|
346
|
-
const restartOnFailure = snapshot.runtimeConnection.restartOnFailure;
|
|
347
319
|
const controlPlaneEnabled = snapshot.runtimeEndpoints.controlPlane.enabled;
|
|
348
320
|
const listenerEnabled = snapshot.runtimeEndpoints.httpListener.enabled;
|
|
349
321
|
const webEnabled = snapshot.runtimeEndpoints.web.enabled;
|
|
@@ -414,8 +386,7 @@ export function renderCliStatus(options: CliStatusOptions): string {
|
|
|
414
386
|
'',
|
|
415
387
|
'Connected-Service Config Signals:',
|
|
416
388
|
` host config present: ${yesNo(serviceEnabled)}`,
|
|
417
|
-
|
|
418
|
-
` host restart policy: ${yesNo(restartOnFailure)}`,
|
|
389
|
+
' lifecycle config: external to Agent',
|
|
419
390
|
'',
|
|
420
391
|
'Endpoint Diagnostics:',
|
|
421
392
|
bindLine('runtimeApi', controlPlaneEnabled, controlPlaneBinding),
|
|
@@ -6,6 +6,8 @@ type AgentWorkspaceFieldReader = (fieldId: string) => string;
|
|
|
6
6
|
export type AgentWorkspaceBasicCommandEditorKind = Extract<
|
|
7
7
|
AgentWorkspaceEditorKind,
|
|
8
8
|
'knowledge-file' | 'knowledge-bookmarks' | 'knowledge-browser-history' | 'knowledge-connector-ingest' | 'tts-prompt' | 'image-input' | 'skill-bundle' | 'skill-discovery-import' | 'profile-template-export' | 'profile-template-import'
|
|
9
|
+
| 'persona-discovery-import'
|
|
10
|
+
| 'routine-discovery-import'
|
|
9
11
|
| 'mcp-server' | 'notify-webhook' | 'notify-webhook-remove' | 'notify-webhook-test'
|
|
10
12
|
>;
|
|
11
13
|
|
|
@@ -39,6 +41,8 @@ export function isAgentWorkspaceBasicCommandEditorKind(kind: AgentWorkspaceEdito
|
|
|
39
41
|
|| kind === 'tts-prompt'
|
|
40
42
|
|| kind === 'image-input'
|
|
41
43
|
|| kind === 'skill-bundle'
|
|
44
|
+
|| kind === 'persona-discovery-import'
|
|
45
|
+
|| kind === 'routine-discovery-import'
|
|
42
46
|
|| kind === 'skill-discovery-import'
|
|
43
47
|
|| kind === 'profile-template-export'
|
|
44
48
|
|| kind === 'profile-template-import'
|
|
@@ -236,6 +240,34 @@ export function createAgentWorkspaceBasicCommandEditor(kind: AgentWorkspaceBasic
|
|
|
236
240
|
],
|
|
237
241
|
};
|
|
238
242
|
}
|
|
243
|
+
if (kind === 'persona-discovery-import') {
|
|
244
|
+
return {
|
|
245
|
+
kind,
|
|
246
|
+
mode: 'create',
|
|
247
|
+
title: 'Import Discovered Persona',
|
|
248
|
+
selectedFieldIndex: 0,
|
|
249
|
+
message: 'Import one discovered persona markdown file into the Agent-local persona registry. Type yes on the final field to confirm.',
|
|
250
|
+
fields: [
|
|
251
|
+
{ id: 'name', label: 'Discovered persona', value: '', required: true, multiline: false, hint: 'Name shown by /personas discover.' },
|
|
252
|
+
{ id: 'use', label: 'Use now', value: 'yes', required: false, multiline: false, hint: 'yes/no.' },
|
|
253
|
+
{ id: 'confirm', label: 'Confirm', value: '', required: true, multiline: false, hint: 'Type yes to run /personas import-discovered with --yes.' },
|
|
254
|
+
],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (kind === 'routine-discovery-import') {
|
|
258
|
+
return {
|
|
259
|
+
kind,
|
|
260
|
+
mode: 'create',
|
|
261
|
+
title: 'Import Discovered Routine',
|
|
262
|
+
selectedFieldIndex: 0,
|
|
263
|
+
message: 'Import one discovered routine markdown file into the Agent-local routine registry. Type yes on the final field to confirm.',
|
|
264
|
+
fields: [
|
|
265
|
+
{ id: 'name', label: 'Discovered routine', value: '', required: true, multiline: false, hint: 'Name shown by /routines discover.' },
|
|
266
|
+
{ id: 'enabled', label: 'Enable now', value: 'yes', required: false, multiline: false, hint: 'yes/no.' },
|
|
267
|
+
{ id: 'confirm', label: 'Confirm', value: '', required: true, multiline: false, hint: 'Type yes to run /routines import-discovered with --yes.' },
|
|
268
|
+
],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
239
271
|
return {
|
|
240
272
|
kind,
|
|
241
273
|
mode: 'create',
|
|
@@ -595,6 +627,64 @@ export function buildAgentWorkspaceBasicCommandEditorSubmission(
|
|
|
595
627
|
},
|
|
596
628
|
};
|
|
597
629
|
}
|
|
630
|
+
if (editor.kind === 'persona-discovery-import') {
|
|
631
|
+
if (!isAffirmative(readField('confirm'))) {
|
|
632
|
+
return {
|
|
633
|
+
kind: 'editor',
|
|
634
|
+
editor: { ...editor, message: 'Discovered persona import not confirmed. Type yes, then press Enter.' },
|
|
635
|
+
status: 'Agent persona import not confirmed.',
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
const parts = [
|
|
639
|
+
'/personas',
|
|
640
|
+
'import-discovered',
|
|
641
|
+
quoteSlashCommandArg(readField('name')),
|
|
642
|
+
];
|
|
643
|
+
if (isAffirmative(readField('use'))) parts.push('--use');
|
|
644
|
+
parts.push('--yes');
|
|
645
|
+
const command = parts.join(' ');
|
|
646
|
+
return {
|
|
647
|
+
kind: 'dispatch',
|
|
648
|
+
command,
|
|
649
|
+
status: 'Opening discovered persona import.',
|
|
650
|
+
actionResult: {
|
|
651
|
+
kind: 'dispatched',
|
|
652
|
+
title: 'Opening discovered persona import',
|
|
653
|
+
detail: 'The workspace handed a confirmed local persona import command to the shell-owned command router.',
|
|
654
|
+
command,
|
|
655
|
+
safety: 'safe',
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (editor.kind === 'routine-discovery-import') {
|
|
660
|
+
if (!isAffirmative(readField('confirm'))) {
|
|
661
|
+
return {
|
|
662
|
+
kind: 'editor',
|
|
663
|
+
editor: { ...editor, message: 'Discovered routine import not confirmed. Type yes, then press Enter.' },
|
|
664
|
+
status: 'Agent routine import not confirmed.',
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const parts = [
|
|
668
|
+
'/routines',
|
|
669
|
+
'import-discovered',
|
|
670
|
+
quoteSlashCommandArg(readField('name')),
|
|
671
|
+
];
|
|
672
|
+
if (isAffirmative(readField('enabled'))) parts.push('--enabled');
|
|
673
|
+
parts.push('--yes');
|
|
674
|
+
const command = parts.join(' ');
|
|
675
|
+
return {
|
|
676
|
+
kind: 'dispatch',
|
|
677
|
+
command,
|
|
678
|
+
status: 'Opening discovered routine import.',
|
|
679
|
+
actionResult: {
|
|
680
|
+
kind: 'dispatched',
|
|
681
|
+
title: 'Opening discovered routine import',
|
|
682
|
+
detail: 'The workspace handed a confirmed local routine import command to the shell-owned command router.',
|
|
683
|
+
command,
|
|
684
|
+
safety: 'safe',
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
}
|
|
598
688
|
const commandParts = [
|
|
599
689
|
'/agent-skills bundle create',
|
|
600
690
|
'--name',
|
|
@@ -158,6 +158,8 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
158
158
|
actions: [
|
|
159
159
|
{ id: 'personas-list', label: 'List personas', detail: 'Print the full local persona library.', command: '/personas list', kind: 'command', safety: 'read-only' },
|
|
160
160
|
{ id: 'personas-active', label: 'Show active persona', detail: 'Inspect the active local persona applied to new turns.', command: '/personas active', kind: 'command', safety: 'read-only' },
|
|
161
|
+
{ id: 'personas-discover', label: 'Discover persona files', detail: 'Scan project and global Agent persona folders for persona markdown without importing it.', command: '/personas discover', kind: 'command', safety: 'read-only' },
|
|
162
|
+
{ id: 'personas-import-discovered', label: 'Import discovered persona', detail: 'Open an in-workspace form that imports one reviewed persona markdown file into the Agent-local persona registry after typed confirmation.', editorKind: 'persona-discovery-import', kind: 'editor', safety: 'safe' },
|
|
161
163
|
{ id: 'personas-prev', label: 'Previous persona', detail: 'Move the local persona selection up without changing active state.', localKind: 'persona', selectionDelta: -1, kind: 'local-selection', safety: 'safe' },
|
|
162
164
|
{ id: 'personas-next', label: 'Next persona', detail: 'Move the local persona selection down without changing active state.', localKind: 'persona', selectionDelta: 1, kind: 'local-selection', safety: 'safe' },
|
|
163
165
|
{ id: 'personas-create', label: 'Create persona', detail: 'Open an in-workspace form for a local persona that writes Agent-local state only.', editorKind: 'persona', kind: 'editor', safety: 'safe' },
|
|
@@ -200,6 +202,8 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
200
202
|
actions: [
|
|
201
203
|
{ id: 'routines-list', label: 'List routines', detail: 'Print the full local Agent routine library.', command: '/routines list', kind: 'command', safety: 'read-only' },
|
|
202
204
|
{ id: 'routines-enabled', label: 'Enabled routines', detail: 'Show routines available for direct use.', command: '/routines enabled', kind: 'command', safety: 'read-only' },
|
|
205
|
+
{ id: 'routines-discover', label: 'Discover routine files', detail: 'Scan project and global Agent routine folders for routine markdown without importing it.', command: '/routines discover', kind: 'command', safety: 'read-only' },
|
|
206
|
+
{ id: 'routines-import-discovered', label: 'Import discovered routine', detail: 'Open an in-workspace form that imports one reviewed routine markdown file into the Agent-local routine registry after typed confirmation.', editorKind: 'routine-discovery-import', kind: 'editor', safety: 'safe' },
|
|
203
207
|
{ id: 'routines-prev', label: 'Previous routine', detail: 'Move the local routine selection up without changing enabled state.', localKind: 'routine', selectionDelta: -1, kind: 'local-selection', safety: 'safe' },
|
|
204
208
|
{ id: 'routines-next', label: 'Next routine', detail: 'Move the local routine selection down without changing enabled state.', localKind: 'routine', selectionDelta: 1, kind: 'local-selection', safety: 'safe' },
|
|
205
209
|
{ id: 'routines-create', label: 'Create routine', detail: 'Open an in-workspace form for a repeatable local workflow that writes Agent-local state only.', editorKind: 'routine', kind: 'editor', safety: 'safe' },
|
|
@@ -24,6 +24,8 @@ type AgentWorkspaceCommandEditorKind = Extract<
|
|
|
24
24
|
| 'tts-prompt'
|
|
25
25
|
| 'image-input'
|
|
26
26
|
| 'skill-bundle'
|
|
27
|
+
| 'persona-discovery-import'
|
|
28
|
+
| 'routine-discovery-import'
|
|
27
29
|
| 'skill-discovery-import'
|
|
28
30
|
| 'profile-template-export'
|
|
29
31
|
| 'profile-template-import'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { discoverPersonas, type DiscoveredPersonaRecord } from '../../agent/persona-discovery.ts';
|
|
1
2
|
import { AgentPersonaRegistry, type AgentPersonaRecord } from '../../agent/persona-registry.ts';
|
|
2
3
|
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
3
4
|
import { requireShellPaths } from './runtime-services.ts';
|
|
@@ -82,6 +83,82 @@ function renderPersona(persona: AgentPersonaRecord, activePersonaId: string | nu
|
|
|
82
83
|
].filter(Boolean).join('\n');
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
function summarizeDiscoveredPersona(persona: DiscoveredPersonaRecord): string {
|
|
87
|
+
const description = persona.description ? ` - ${persona.description}` : '';
|
|
88
|
+
return ` ${persona.name} ${persona.origin}${description}\n path: ${persona.path}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderDiscoveredPersonas(personas: readonly DiscoveredPersonaRecord[]): string {
|
|
92
|
+
if (personas.length === 0) {
|
|
93
|
+
return [
|
|
94
|
+
'Discovered Agent persona files',
|
|
95
|
+
' No persona markdown files found in project/global Agent persona folders.',
|
|
96
|
+
' Search roots: .goodvibes/personas, .goodvibes/agent/personas, .goodvibes/agents, ~/.goodvibes/personas, ~/.goodvibes/agent/personas, ~/.goodvibes/agents',
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
return [
|
|
100
|
+
`Discovered Agent persona files (${personas.length})`,
|
|
101
|
+
...personas.map(summarizeDiscoveredPersona),
|
|
102
|
+
'',
|
|
103
|
+
'Import one with: /personas import-discovered <name> --yes',
|
|
104
|
+
].join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function discoveredPersonaLookupValues(persona: DiscoveredPersonaRecord): readonly string[] {
|
|
108
|
+
const slug = persona.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
109
|
+
const basename = persona.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
|
|
110
|
+
return [persona.name, slug, persona.path, basename].map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function findDiscoveredPersona(personas: readonly DiscoveredPersonaRecord[], idOrName: string): DiscoveredPersonaRecord | null {
|
|
114
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
115
|
+
if (!lookup) return null;
|
|
116
|
+
return personas.find((persona) => discoveredPersonaLookupValues(persona).includes(lookup)) ?? null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function frontmatterList(persona: DiscoveredPersonaRecord, key: string): readonly string[] {
|
|
120
|
+
const value = persona.frontmatter[key];
|
|
121
|
+
if (!value) return [];
|
|
122
|
+
return splitList(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function importDiscoveredPersona(args: readonly string[], ctx: CommandContext, personaRegistry: AgentPersonaRegistry): Promise<void> {
|
|
126
|
+
const parsed = parsePersonaArgs(args);
|
|
127
|
+
const name = parsed.rest.join(' ').trim();
|
|
128
|
+
if (!name) {
|
|
129
|
+
ctx.print('Usage: /personas import-discovered <name> [--use] --yes');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const discovered = findDiscoveredPersona(await discoverPersonas(requireShellPaths(ctx)), name);
|
|
133
|
+
if (!discovered) {
|
|
134
|
+
ctx.print(`Unknown discovered Agent persona: ${name}\nRun /personas discover to inspect available persona files.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!parsed.yes) {
|
|
138
|
+
ctx.print([
|
|
139
|
+
'Agent persona import preview',
|
|
140
|
+
` name: ${discovered.name}`,
|
|
141
|
+
` origin: ${discovered.origin}`,
|
|
142
|
+
` path: ${discovered.path}`,
|
|
143
|
+
` description: ${discovered.description || '(none)'}`,
|
|
144
|
+
` body characters: ${discovered.body.length}`,
|
|
145
|
+
' next: rerun with --yes to import into the Agent-local persona registry',
|
|
146
|
+
].join('\n'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const persona = personaRegistry.create({
|
|
150
|
+
name: discovered.name,
|
|
151
|
+
description: discovered.description || `Imported persona from ${discovered.origin} markdown file.`,
|
|
152
|
+
body: discovered.body,
|
|
153
|
+
tags: frontmatterList(discovered, 'tags'),
|
|
154
|
+
triggers: frontmatterList(discovered, 'triggers'),
|
|
155
|
+
source: 'imported',
|
|
156
|
+
provenance: `discovered:${discovered.origin}:${discovered.path}`,
|
|
157
|
+
});
|
|
158
|
+
if (parsed.flags.get('use') === 'true') personaRegistry.setActive(persona.id);
|
|
159
|
+
ctx.print(`Imported Agent persona ${persona.id}: ${persona.name}${parsed.flags.get('use') === 'true' ? ' (active)' : ''}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
85
162
|
function requiredFlag(flags: ReadonlyMap<string, string>, key: string): string {
|
|
86
163
|
const value = flags.get(key)?.trim();
|
|
87
164
|
if (!value) throw new Error(`Missing --${key}.`);
|
|
@@ -97,7 +174,7 @@ export function registerPersonasRuntimeCommands(registry: CommandRegistry): void
|
|
|
97
174
|
name: 'personas',
|
|
98
175
|
aliases: ['persona'],
|
|
99
176
|
description: 'Manage local GoodVibes Agent personas',
|
|
100
|
-
usage: '[list|search <query>|show <id>|create --name <name> --description <summary> --body <instructions>|update <id> [--name ...] [--description ...] [--body ...]|use <id>|active|clear|review <id>|stale <id> <reason...>|delete <id> --yes]',
|
|
177
|
+
usage: '[list|discover|import-discovered <name> --yes|search <query>|show <id>|create --name <name> --description <summary> --body <instructions>|update <id> [--name ...] [--description ...] [--body ...]|use <id>|active|clear|review <id>|stale <id> <reason...>|delete <id> --yes]',
|
|
101
178
|
async handler(args, ctx) {
|
|
102
179
|
const sub = (args[0] ?? 'list').toLowerCase();
|
|
103
180
|
const registryStore = registryFromContext(ctx);
|
|
@@ -106,6 +183,14 @@ export function registerPersonasRuntimeCommands(registry: CommandRegistry): void
|
|
|
106
183
|
ctx.print(renderList('Agent Personas', registryStore, registryStore.list()));
|
|
107
184
|
return;
|
|
108
185
|
}
|
|
186
|
+
if (sub === 'discover' || sub === 'discovered') {
|
|
187
|
+
ctx.print(renderDiscoveredPersonas(await discoverPersonas(requireShellPaths(ctx))));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (sub === 'import-discovered' || sub === 'import-persona') {
|
|
191
|
+
await importDiscoveredPersona(args.slice(1), ctx, registryStore);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
109
194
|
if (sub === 'search') {
|
|
110
195
|
const query = args.slice(1).join(' ').trim();
|
|
111
196
|
ctx.print(renderList(query ? `Agent Personas matching "${query}"` : 'Agent Personas', registryStore, registryStore.search(query)));
|
|
@@ -210,7 +295,7 @@ export function registerPersonasRuntimeCommands(registry: CommandRegistry): void
|
|
|
210
295
|
ctx.print(`Deleted Agent persona ${removed.id}: ${removed.name}`);
|
|
211
296
|
return;
|
|
212
297
|
}
|
|
213
|
-
ctx.print('Usage: /personas [list|search <query>|show <id>|create|update|use|active|clear|review|stale|delete]');
|
|
298
|
+
ctx.print('Usage: /personas [list|discover|import-discovered|search <query>|show <id>|create|update|use|active|clear|review|stale|delete]');
|
|
214
299
|
} catch (error) {
|
|
215
300
|
printError(ctx, error);
|
|
216
301
|
}
|