@pellux/goodvibes-agent 0.1.35 → 0.1.37
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 +8 -0
- package/README.md +5 -0
- package/docs/README.md +1 -1
- package/docs/getting-started.md +4 -1
- package/docs/operator-capability-benchmark.md +4 -3
- package/package.json +1 -1
- package/src/agent/routine-schedule-promotion.ts +434 -0
- package/src/agent/runtime-profile.ts +4 -0
- package/src/cli/completion.ts +1 -0
- package/src/cli/help.ts +17 -0
- package/src/cli/management.ts +6 -0
- package/src/cli/parser.ts +2 -0
- package/src/cli/routines-command.ts +185 -0
- package/src/cli/types.ts +1 -0
- package/src/input/agent-workspace.ts +20 -6
- package/src/input/commands/agent-runtime-profile-runtime.ts +223 -0
- package/src/input/commands/routines-runtime.ts +40 -2
- package/src/input/commands/schedule-runtime.ts +52 -8
- package/src/input/commands.ts +2 -0
- package/src/operator/capability-benchmark.ts +5 -5
- package/src/renderer/agent-workspace.ts +2 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createShellPathService } from '@/runtime/index.ts';
|
|
2
|
+
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
|
|
3
|
+
import {
|
|
4
|
+
buildRoutineSchedulePreview,
|
|
5
|
+
formatRoutineScheduleFailure,
|
|
6
|
+
formatRoutineSchedulePreview,
|
|
7
|
+
formatRoutineScheduleSuccess,
|
|
8
|
+
parseRoutineSchedulePromotionArgs,
|
|
9
|
+
promoteRoutineToDaemonSchedule,
|
|
10
|
+
resolveAgentDaemonConnection,
|
|
11
|
+
} from '../agent/routine-schedule-promotion.ts';
|
|
12
|
+
import type { CliCommandOutput } from './types.ts';
|
|
13
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
14
|
+
|
|
15
|
+
interface RoutinesCommandSuccess<TData> {
|
|
16
|
+
readonly ok: true;
|
|
17
|
+
readonly kind: string;
|
|
18
|
+
readonly data: TData;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface RoutinesCommandFailure {
|
|
22
|
+
readonly ok: false;
|
|
23
|
+
readonly kind: string;
|
|
24
|
+
readonly error: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
|
|
28
|
+
return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function routineRegistry(runtime: CliCommandRuntime): AgentRoutineRegistry {
|
|
32
|
+
return AgentRoutineRegistry.fromShellPaths(createShellPathService({
|
|
33
|
+
workingDirectory: runtime.workingDirectory,
|
|
34
|
+
homeDirectory: runtime.homeDirectory,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function summarizeRoutine(routine: AgentRoutineRecord): string {
|
|
39
|
+
const enabled = routine.enabled ? 'enabled' : 'disabled';
|
|
40
|
+
const tags = routine.tags.length > 0 ? ` tags=${routine.tags.join(',')}` : '';
|
|
41
|
+
return ` ${routine.id} ${enabled} ${routine.reviewState} starts=${routine.startCount} ${routine.name} - ${routine.description}${tags}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderRoutineList(title: string, path: string, routines: readonly AgentRoutineRecord[]): string {
|
|
45
|
+
if (routines.length === 0) {
|
|
46
|
+
return [
|
|
47
|
+
title,
|
|
48
|
+
' No local Agent routines yet.',
|
|
49
|
+
' Create routines inside the Agent TUI with /routines create, or create a runtime profile from a starter template.',
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
`${title} (${routines.length})`,
|
|
54
|
+
` store: ${path}`,
|
|
55
|
+
...routines.map(summarizeRoutine),
|
|
56
|
+
].join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderRoutine(routine: AgentRoutineRecord): string {
|
|
60
|
+
return [
|
|
61
|
+
`Routine ${routine.name}`,
|
|
62
|
+
` id: ${routine.id}`,
|
|
63
|
+
` enabled: ${routine.enabled ? 'yes' : 'no'}`,
|
|
64
|
+
` review: ${routine.reviewState}`,
|
|
65
|
+
` source: ${routine.source}`,
|
|
66
|
+
` provenance: ${routine.provenance}`,
|
|
67
|
+
` tags: ${routine.tags.join(', ') || '(none)'}`,
|
|
68
|
+
` triggers: ${routine.triggers.join(', ') || '(manual)'}`,
|
|
69
|
+
` started: ${routine.startCount}${routine.lastStartedAt ? `; last ${routine.lastStartedAt}` : ''}`,
|
|
70
|
+
` created: ${routine.createdAt}`,
|
|
71
|
+
` updated: ${routine.updatedAt}`,
|
|
72
|
+
routine.staleReason ? ` stale reason: ${routine.staleReason}` : '',
|
|
73
|
+
'',
|
|
74
|
+
routine.description,
|
|
75
|
+
'',
|
|
76
|
+
routine.steps,
|
|
77
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handleRoutinePromotion(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
81
|
+
const parsed = parseRoutineSchedulePromotionArgs(args);
|
|
82
|
+
const json = runtime.cli.flags.outputFormat === 'json';
|
|
83
|
+
if (parsed.errors.length > 0) {
|
|
84
|
+
const failure: RoutinesCommandFailure = {
|
|
85
|
+
ok: false,
|
|
86
|
+
kind: 'invalid_routine_schedule_promotion',
|
|
87
|
+
error: parsed.errors.join(' '),
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
output: json ? JSON.stringify(failure, null, 2) : [
|
|
91
|
+
'Usage: goodvibes-agent routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
|
|
92
|
+
...parsed.errors.map((error) => ` ${error}`),
|
|
93
|
+
].join('\n'),
|
|
94
|
+
exitCode: 2,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const registry = routineRegistry(runtime);
|
|
98
|
+
const routine = registry.get(parsed.routineId ?? '');
|
|
99
|
+
if (!routine) {
|
|
100
|
+
const failure: RoutinesCommandFailure = {
|
|
101
|
+
ok: false,
|
|
102
|
+
kind: 'routine_not_found',
|
|
103
|
+
error: `Unknown Agent routine: ${parsed.routineId ?? ''}`,
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
output: json ? JSON.stringify(failure, null, 2) : failure.error,
|
|
107
|
+
exitCode: 1,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const preview = buildRoutineSchedulePreview(routine, parsed);
|
|
111
|
+
if (!parsed.yes) {
|
|
112
|
+
const value: RoutinesCommandSuccess<typeof preview> = {
|
|
113
|
+
ok: true,
|
|
114
|
+
kind: 'schedules.create.preview',
|
|
115
|
+
data: preview,
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
output: jsonOrText(runtime, value, formatRoutineSchedulePreview(preview)),
|
|
119
|
+
exitCode: 0,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const connection = resolveAgentDaemonConnection(runtime.configManager, runtime.homeDirectory);
|
|
123
|
+
const result = await promoteRoutineToDaemonSchedule(connection, preview);
|
|
124
|
+
if (!result.ok) {
|
|
125
|
+
return {
|
|
126
|
+
output: json ? JSON.stringify(result, null, 2) : formatRoutineScheduleFailure(result),
|
|
127
|
+
exitCode: 1,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
output: jsonOrText(runtime, result, formatRoutineScheduleSuccess(result)),
|
|
132
|
+
exitCode: 0,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
137
|
+
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
138
|
+
const registry = routineRegistry(runtime);
|
|
139
|
+
const snapshot = registry.snapshot();
|
|
140
|
+
const normalized = sub.toLowerCase();
|
|
141
|
+
if (normalized === 'list' || normalized === 'enabled') {
|
|
142
|
+
const routines = normalized === 'enabled' ? snapshot.enabledRoutines : snapshot.routines;
|
|
143
|
+
const value: RoutinesCommandSuccess<{
|
|
144
|
+
readonly path: string;
|
|
145
|
+
readonly routines: readonly AgentRoutineRecord[];
|
|
146
|
+
readonly enabledCount: number;
|
|
147
|
+
}> = {
|
|
148
|
+
ok: true,
|
|
149
|
+
kind: normalized === 'enabled' ? 'agent.routines.enabled' : 'agent.routines.list',
|
|
150
|
+
data: {
|
|
151
|
+
path: snapshot.path,
|
|
152
|
+
routines,
|
|
153
|
+
enabledCount: snapshot.enabledRoutines.length,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
output: jsonOrText(runtime, value, renderRoutineList(normalized === 'enabled' ? 'Enabled Agent routines' : 'Agent routines', snapshot.path, routines)),
|
|
158
|
+
exitCode: 0,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (normalized === 'show') {
|
|
162
|
+
const id = rest[0];
|
|
163
|
+
if (!id) return { output: 'Usage: goodvibes-agent routines show <id>', exitCode: 2 };
|
|
164
|
+
const routine = registry.get(id);
|
|
165
|
+
if (!routine) {
|
|
166
|
+
const failure: RoutinesCommandFailure = { ok: false, kind: 'routine_not_found', error: `Unknown Agent routine: ${id}` };
|
|
167
|
+
return {
|
|
168
|
+
output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(failure, null, 2) : failure.error,
|
|
169
|
+
exitCode: 1,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const value: RoutinesCommandSuccess<AgentRoutineRecord> = { ok: true, kind: 'agent.routines.show', data: routine };
|
|
173
|
+
return {
|
|
174
|
+
output: jsonOrText(runtime, value, renderRoutine(routine)),
|
|
175
|
+
exitCode: 0,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (normalized === 'promote' || normalized === 'schedule' || normalized === 'promote-schedule') {
|
|
179
|
+
return handleRoutinePromotion(runtime, rest);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes]',
|
|
183
|
+
exitCode: 2,
|
|
184
|
+
};
|
|
185
|
+
}
|
package/src/cli/types.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { CommandContext } from './command-registry.ts';
|
|
|
4
4
|
import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
|
|
5
5
|
import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
|
|
6
6
|
import { AgentSkillRegistry } from '../agent/skill-registry.ts';
|
|
7
|
-
import { getAgentRuntimeProfilesRoot, listAgentRuntimeProfiles } from '../agent/runtime-profile.ts';
|
|
7
|
+
import { getAgentRuntimeProfilesRoot, listAgentRuntimeProfiles, listAgentRuntimeProfileTemplates } from '../agent/runtime-profile.ts';
|
|
8
8
|
|
|
9
9
|
export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
|
|
10
10
|
|
|
@@ -97,6 +97,8 @@ export interface AgentWorkspaceRuntimeSnapshot {
|
|
|
97
97
|
readonly activeRuntimeProfile: string;
|
|
98
98
|
readonly runtimeProfileCount: number;
|
|
99
99
|
readonly runtimeProfileRoot: string;
|
|
100
|
+
readonly runtimeStarterTemplateCount: number;
|
|
101
|
+
readonly localStarterTemplateCount: number;
|
|
100
102
|
readonly configProfileCount: number;
|
|
101
103
|
readonly warnings: readonly string[];
|
|
102
104
|
}
|
|
@@ -378,6 +380,13 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
378
380
|
return 0;
|
|
379
381
|
}
|
|
380
382
|
})();
|
|
383
|
+
const runtimeStarterTemplates = (() => {
|
|
384
|
+
try {
|
|
385
|
+
return listAgentRuntimeProfileTemplates(context.workspace?.shellPaths?.homeDirectory ?? '');
|
|
386
|
+
} catch {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
})();
|
|
381
390
|
const voiceProviders = (() => {
|
|
382
391
|
try {
|
|
383
392
|
return context.platform?.voiceProviderRegistry?.list?.() ?? [];
|
|
@@ -437,6 +446,8 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
437
446
|
activeRuntimeProfile: inferActiveRuntimeProfile(context.workspace?.shellPaths?.homeDirectory ?? ''),
|
|
438
447
|
runtimeProfileCount: runtimeProfiles.length,
|
|
439
448
|
runtimeProfileRoot: getAgentRuntimeProfilesRoot(context.workspace?.shellPaths?.homeDirectory ?? ''),
|
|
449
|
+
runtimeStarterTemplateCount: runtimeStarterTemplates.length,
|
|
450
|
+
localStarterTemplateCount: runtimeStarterTemplates.filter((template) => template.source === 'local').length,
|
|
440
451
|
configProfileCount,
|
|
441
452
|
warnings,
|
|
442
453
|
};
|
|
@@ -522,11 +533,13 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
522
533
|
detail: 'Hermes profiles isolate agent state. GoodVibes Agent exposes named runtime homes, config profile pickers, profile-sync bundles, setup transfer bundles, and support bundles while keeping the daemon external.',
|
|
523
534
|
actions: [
|
|
524
535
|
{ id: 'profiles-open', label: 'Open config profiles', detail: 'Open the TUI-derived config profile picker for display/provider/behavior profile files.', command: '/profiles', kind: 'command', safety: 'safe' },
|
|
536
|
+
{ id: 'runtime-profile-guide', label: 'Starter authoring guide', detail: 'Open the Agent-local starter authoring flow inside the TUI command surface.', command: '/agent-profile guide', kind: 'command', safety: 'safe' },
|
|
537
|
+
{ id: 'runtime-profile-templates', label: 'Browse starter templates', detail: 'List built-in and local Agent starter templates with persona, skill, routine, and source details.', command: '/agent-profile templates', kind: 'command', safety: 'read-only' },
|
|
525
538
|
{ id: 'profile-sync-list', label: 'Profile sync list', detail: 'Inspect saved config profiles available for export/import.', command: '/profilesync list', kind: 'command', safety: 'read-only' },
|
|
526
539
|
{ id: 'profile-sync-export', label: 'Export profile sync', detail: 'Export config profiles to a portable bundle. Requires a real path and explicit --yes.', command: '/profilesync export <path> --yes', kind: 'command', safety: 'safe' },
|
|
527
540
|
{ id: 'setup-transfer-export', label: 'Export setup transfer', detail: 'Export Agent setup transfer data from the current home. Requires a real path and explicit --yes.', command: '/setup transfer export <path> --yes', kind: 'command', safety: 'safe' },
|
|
528
|
-
{ id: 'runtime-profile-create', label: 'Create runtime profile', detail: '
|
|
529
|
-
{ id: 'runtime-profile-template-edit', label: 'Customize starter', detail: '
|
|
541
|
+
{ id: 'runtime-profile-create', label: 'Create runtime profile', detail: 'Create an isolated Agent runtime profile from a built-in or local starter. Requires a real name and explicit --yes.', command: '/agent-profile create <name> --template <id> --yes', kind: 'command', safety: 'safe' },
|
|
542
|
+
{ id: 'runtime-profile-template-edit', label: 'Customize starter', detail: 'Export a starter JSON file, edit it, import it as a local starter, then create a profile from it.', command: '/agent-profile template export <id> <path> --yes', kind: 'command', safety: 'safe' },
|
|
530
543
|
{ id: 'runtime-profile-switch', label: 'Switch runtime profile', detail: 'Launch goodvibes-agent --agent-profile <name> to use that isolated Agent home. This workspace cannot switch the current process home after startup.', kind: 'guidance', safety: 'safe' },
|
|
531
544
|
],
|
|
532
545
|
},
|
|
@@ -559,11 +572,12 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
559
572
|
id: 'automation',
|
|
560
573
|
group: 'WATCH',
|
|
561
574
|
label: 'Automation',
|
|
562
|
-
summary: '
|
|
563
|
-
detail: 'Agent does not create
|
|
575
|
+
summary: 'Automation and schedule observability with explicit routine promotion.',
|
|
576
|
+
detail: 'Agent does not create local automation jobs or hidden scheduler spawns. Reviewed local routines can be promoted into externally owned daemon schedules only through an explicit schedules.create command with --yes.',
|
|
564
577
|
actions: [
|
|
565
578
|
{ id: 'schedule-list', label: 'List schedules', detail: 'Inspect configured jobs and history without running or mutating them.', command: '/schedule list', kind: 'command', safety: 'read-only' },
|
|
566
|
-
{ id: 'schedule-
|
|
579
|
+
{ id: 'schedule-promote-routine', label: 'Promote routine', detail: 'Create an external daemon schedule from a local Agent routine. Requires a real routine id, schedule expression, and explicit --yes.', command: '/schedule promote-routine <routine-id> --cron <expr> --yes', kind: 'command', safety: 'safe' },
|
|
580
|
+
{ id: 'schedule-policy', label: 'Local scheduler blocked', detail: 'Local schedule add/run/remove/enable/disable remain blocked; only explicit external daemon schedule promotion is allowed here.', kind: 'guidance', safety: 'blocked' },
|
|
567
581
|
{ id: 'health-services', label: 'Service health', detail: 'Inspect service readiness without starting, stopping, or restarting daemon services.', command: '/health services', kind: 'command', safety: 'read-only' },
|
|
568
582
|
],
|
|
569
583
|
},
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
createAgentRuntimeProfile,
|
|
5
|
+
deleteAgentRuntimeProfile,
|
|
6
|
+
exportAgentRuntimeProfileTemplate,
|
|
7
|
+
getAgentRuntimeProfileTemplateFile,
|
|
8
|
+
importAgentRuntimeProfileTemplate,
|
|
9
|
+
listAgentRuntimeProfiles,
|
|
10
|
+
listAgentRuntimeProfileTemplates,
|
|
11
|
+
type AgentRuntimeProfileInfo,
|
|
12
|
+
type AgentRuntimeProfileTemplateSummary,
|
|
13
|
+
} from '../../agent/runtime-profile.ts';
|
|
14
|
+
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
15
|
+
import { requireYesFlag, stripYesFlag } from './confirmation.ts';
|
|
16
|
+
import { requireShellPaths } from './runtime-services.ts';
|
|
17
|
+
|
|
18
|
+
function parseFlag(args: readonly string[], name: string): string | undefined {
|
|
19
|
+
const index = args.indexOf(name);
|
|
20
|
+
if (index < 0) return undefined;
|
|
21
|
+
const value = args[index + 1];
|
|
22
|
+
return value && !value.startsWith('--') ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function profileLine(profile: AgentRuntimeProfileInfo): string {
|
|
26
|
+
const created = profile.createdAt ? ` created=${profile.createdAt}` : '';
|
|
27
|
+
const starter = profile.starterTemplateId ? ` starter=${profile.starterTemplateId}` : '';
|
|
28
|
+
return ` ${profile.id} home=${profile.homeDirectory}${created}${starter}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function templateLine(template: AgentRuntimeProfileTemplateSummary): string {
|
|
32
|
+
const source = template.source === 'local' ? `local ${template.path ?? ''}`.trim() : 'builtin';
|
|
33
|
+
return [
|
|
34
|
+
` ${template.id} [${source}]`,
|
|
35
|
+
` ${template.name}: ${template.description}`,
|
|
36
|
+
` persona: ${template.personaName}`,
|
|
37
|
+
` skills: ${template.skillNames.join(', ')}`,
|
|
38
|
+
` routines: ${template.routineNames.join(', ')}`,
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderProfiles(homeDirectory: string): string {
|
|
43
|
+
const profiles = listAgentRuntimeProfiles(homeDirectory);
|
|
44
|
+
if (profiles.length === 0) {
|
|
45
|
+
return [
|
|
46
|
+
'Agent Runtime Profiles',
|
|
47
|
+
' No isolated Agent runtime profiles yet.',
|
|
48
|
+
' Create one with /agent-profile create <name> --template <id> --yes.',
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
return ['Agent Runtime Profiles', ...profiles.map(profileLine)].join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderTemplates(homeDirectory: string): string {
|
|
55
|
+
const templates = listAgentRuntimeProfileTemplates(homeDirectory);
|
|
56
|
+
return [
|
|
57
|
+
`Agent Starter Templates (${templates.length})`,
|
|
58
|
+
...templates.map(templateLine),
|
|
59
|
+
'',
|
|
60
|
+
'Authoring flow:',
|
|
61
|
+
' /agent-profile template export research ./agent-starter.json --yes',
|
|
62
|
+
' edit the JSON file',
|
|
63
|
+
' /agent-profile template import ./agent-starter.json --yes',
|
|
64
|
+
' /agent-profile create <name> --template <imported-id> --yes',
|
|
65
|
+
].join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderGuide(homeDirectory: string): string {
|
|
69
|
+
const templates = listAgentRuntimeProfileTemplates(homeDirectory);
|
|
70
|
+
const localCount = templates.filter((template) => template.source === 'local').length;
|
|
71
|
+
return [
|
|
72
|
+
'Agent Starter Authoring Guide',
|
|
73
|
+
` built-in starters: ${templates.length - localCount}`,
|
|
74
|
+
` local starters: ${localCount}`,
|
|
75
|
+
'',
|
|
76
|
+
'1. Pick a base starter:',
|
|
77
|
+
' /agent-profile templates',
|
|
78
|
+
'2. Export a starter JSON file:',
|
|
79
|
+
' /agent-profile template export research ./agent-starter.json --yes',
|
|
80
|
+
'3. Edit id, name, description, persona, skills, and routines in that JSON file.',
|
|
81
|
+
'4. Import it into this Agent home:',
|
|
82
|
+
' /agent-profile template import ./agent-starter.json --yes',
|
|
83
|
+
'5. Create a runtime profile from the imported starter:',
|
|
84
|
+
' /agent-profile create <name> --template <imported-id> --yes',
|
|
85
|
+
'',
|
|
86
|
+
'This writes only Agent-local starter/profile state. It does not mutate the daemon, default wiki, or HomeGraph.',
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderTemplatePreview(homeDirectory: string, templateId: string): string {
|
|
91
|
+
const file = getAgentRuntimeProfileTemplateFile(templateId, homeDirectory);
|
|
92
|
+
return [
|
|
93
|
+
`Agent Starter Template: ${file.template.id}`,
|
|
94
|
+
` name: ${file.template.name}`,
|
|
95
|
+
` source: ${file.template.source}`,
|
|
96
|
+
` description: ${file.template.description}`,
|
|
97
|
+
` persona: ${file.template.persona.name}`,
|
|
98
|
+
` skills: ${file.template.skills.map((skill) => skill.name).join(', ')}`,
|
|
99
|
+
` routines: ${file.template.routines.map((routine) => routine.name).join(', ')}`,
|
|
100
|
+
'',
|
|
101
|
+
'Export/edit/import to customize this starter:',
|
|
102
|
+
` /agent-profile template export ${file.template.id} ./agent-starter.json --yes`,
|
|
103
|
+
].join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function registerAgentRuntimeProfileRuntimeCommands(registry: CommandRegistry): void {
|
|
107
|
+
registry.register({
|
|
108
|
+
name: 'agent-profile',
|
|
109
|
+
aliases: ['runtime-profile', 'agent-profiles'],
|
|
110
|
+
description: 'Manage isolated Agent runtime profiles and starter templates',
|
|
111
|
+
usage: '[list|templates|guide|template show|template export|template import|create|delete]',
|
|
112
|
+
handler(args, ctx) {
|
|
113
|
+
const shellPaths = requireShellPaths(ctx);
|
|
114
|
+
const homeDirectory = shellPaths.homeDirectory;
|
|
115
|
+
const parsed = stripYesFlag(args);
|
|
116
|
+
const commandArgs = [...parsed.rest];
|
|
117
|
+
const sub = (commandArgs[0] ?? 'list').toLowerCase();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (sub === 'list' || sub === 'profiles') {
|
|
121
|
+
ctx.print(renderProfiles(homeDirectory));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (sub === 'templates' || sub === 'starters') {
|
|
126
|
+
ctx.print(renderTemplates(homeDirectory));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (sub === 'guide' || sub === 'author') {
|
|
131
|
+
ctx.print(renderGuide(homeDirectory));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (sub === 'template' || sub === 'starter') {
|
|
136
|
+
const mode = (commandArgs[1] ?? 'show').toLowerCase();
|
|
137
|
+
if (mode === 'show' || mode === 'preview') {
|
|
138
|
+
const templateId = commandArgs[2];
|
|
139
|
+
if (!templateId) {
|
|
140
|
+
ctx.print('Usage: /agent-profile template show <id>');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
ctx.print(renderTemplatePreview(homeDirectory, templateId));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (mode === 'export') {
|
|
147
|
+
const templateId = commandArgs[2];
|
|
148
|
+
const pathArg = commandArgs[3];
|
|
149
|
+
if (!templateId || !pathArg) {
|
|
150
|
+
ctx.print('Usage: /agent-profile template export <id> <path> --yes');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!parsed.yes) {
|
|
154
|
+
requireYesFlag(ctx, `export Agent starter template ${templateId}`, '/agent-profile template export <id> <path> --yes');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const targetPath = shellPaths.resolveWorkspacePath(pathArg);
|
|
158
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
159
|
+
const template = exportAgentRuntimeProfileTemplate(homeDirectory, templateId, targetPath);
|
|
160
|
+
ctx.print(`Agent starter template exported: ${template.id}\n path: ${template.path ?? targetPath}\n edit it, then import it with /agent-profile template import <path> --yes`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (mode === 'import') {
|
|
164
|
+
const pathArg = commandArgs[2];
|
|
165
|
+
if (!pathArg) {
|
|
166
|
+
ctx.print('Usage: /agent-profile template import <path> --yes');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!parsed.yes) {
|
|
170
|
+
requireYesFlag(ctx, 'import Agent starter template', '/agent-profile template import <path> --yes');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const sourcePath = shellPaths.resolveWorkspacePath(pathArg);
|
|
174
|
+
const template = importAgentRuntimeProfileTemplate(homeDirectory, sourcePath);
|
|
175
|
+
ctx.print(`Agent starter template imported: ${template.id}\n source: ${template.source}\n use: /agent-profile create <name> --template ${template.id} --yes`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
ctx.print('Usage: /agent-profile template [show <id>|export <id> <path> --yes|import <path> --yes]');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (sub === 'create') {
|
|
183
|
+
const name = commandArgs[1];
|
|
184
|
+
const templateId = parseFlag(commandArgs, '--template') ?? parseFlag(commandArgs, '--starter');
|
|
185
|
+
if (!name) {
|
|
186
|
+
ctx.print('Usage: /agent-profile create <name> [--template <id>] --yes');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!parsed.yes) {
|
|
190
|
+
requireYesFlag(ctx, `create Agent runtime profile ${name}`, '/agent-profile create <name> [--template <id>] --yes');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const profile = createAgentRuntimeProfile(homeDirectory, name, { templateId });
|
|
194
|
+
ctx.print([
|
|
195
|
+
`Agent runtime profile created: ${profile.id}`,
|
|
196
|
+
` home: ${profile.homeDirectory}`,
|
|
197
|
+
profile.starterTemplateId ? ` starter: ${profile.starterTemplateId}` : '',
|
|
198
|
+
` launch: goodvibes-agent --agent-profile ${profile.id}`,
|
|
199
|
+
].filter(Boolean).join('\n'));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (sub === 'delete') {
|
|
204
|
+
const name = commandArgs[1];
|
|
205
|
+
if (!name) {
|
|
206
|
+
ctx.print('Usage: /agent-profile delete <name> --yes');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!parsed.yes) {
|
|
210
|
+
requireYesFlag(ctx, `delete Agent runtime profile ${name}`, '/agent-profile delete <name> --yes');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
ctx.print(deleteAgentRuntimeProfile(homeDirectory, name) ? `Agent runtime profile deleted: ${name}` : `Agent runtime profile not found: ${name}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
ctx.print('Usage: /agent-profile [list|templates|guide|template show <id>|template export <id> <path> --yes|template import <path> --yes|create <name> [--template <id>] --yes|delete <name> --yes]');
|
|
218
|
+
} catch (error) {
|
|
219
|
+
ctx.print(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../../agent/routine-registry.ts';
|
|
2
|
+
import {
|
|
3
|
+
buildRoutineSchedulePreview,
|
|
4
|
+
formatRoutineScheduleFailure,
|
|
5
|
+
formatRoutineSchedulePreview,
|
|
6
|
+
formatRoutineScheduleSuccess,
|
|
7
|
+
parseRoutineSchedulePromotionArgs,
|
|
8
|
+
promoteRoutineToDaemonSchedule,
|
|
9
|
+
resolveAgentDaemonConnection,
|
|
10
|
+
} from '../../agent/routine-schedule-promotion.ts';
|
|
2
11
|
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
3
12
|
import { requireShellPaths } from './runtime-services.ts';
|
|
4
13
|
|
|
@@ -93,6 +102,31 @@ function printError(ctx: CommandContext, error: unknown): void {
|
|
|
93
102
|
ctx.print(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
async function promoteRoutine(args: readonly string[], routineRegistry: AgentRoutineRegistry, ctx: CommandContext): Promise<void> {
|
|
106
|
+
const parsed = parseRoutineSchedulePromotionArgs(args);
|
|
107
|
+
if (parsed.errors.length > 0) {
|
|
108
|
+
ctx.print([
|
|
109
|
+
'Usage: /routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
|
|
110
|
+
...parsed.errors.map((error) => ` ${error}`),
|
|
111
|
+
].join('\n'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const routine = routineRegistry.get(parsed.routineId ?? '');
|
|
115
|
+
if (!routine) {
|
|
116
|
+
ctx.print(`Unknown Agent routine: ${parsed.routineId ?? ''}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const preview = buildRoutineSchedulePreview(routine, parsed);
|
|
120
|
+
if (!parsed.yes) {
|
|
121
|
+
ctx.print(formatRoutineSchedulePreview(preview));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const shellPaths = requireShellPaths(ctx);
|
|
125
|
+
const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
|
|
126
|
+
const result = await promoteRoutineToDaemonSchedule(connection, preview);
|
|
127
|
+
ctx.print(result.ok ? formatRoutineScheduleSuccess(result) : formatRoutineScheduleFailure(result));
|
|
128
|
+
}
|
|
129
|
+
|
|
96
130
|
export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
|
|
97
131
|
const sub = (args[0] ?? 'list').toLowerCase();
|
|
98
132
|
const routineRegistry = registryFromContext(ctx);
|
|
@@ -200,6 +234,10 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
|
|
|
200
234
|
ctx.print(`Marked Agent routine ${routine.id} stale.`);
|
|
201
235
|
return;
|
|
202
236
|
}
|
|
237
|
+
if (sub === 'promote' || sub === 'schedule' || sub === 'promote-schedule') {
|
|
238
|
+
await promoteRoutine(args.slice(1), routineRegistry, ctx);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
203
241
|
if (sub === 'delete' || sub === 'remove') {
|
|
204
242
|
const parsed = parseRoutineArgs(args.slice(1));
|
|
205
243
|
const id = parsed.rest[0];
|
|
@@ -215,7 +253,7 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
|
|
|
215
253
|
ctx.print(`Deleted Agent routine ${removed.id}: ${removed.name}`);
|
|
216
254
|
return;
|
|
217
255
|
}
|
|
218
|
-
ctx.print('Usage: /routines [list|enabled|search|show|create|update|enable|disable|start|review|stale|delete]');
|
|
256
|
+
ctx.print('Usage: /routines [list|enabled|search|show|create|update|enable|disable|start|review|stale|promote|delete]');
|
|
219
257
|
} catch (error) {
|
|
220
258
|
printError(ctx, error);
|
|
221
259
|
}
|
|
@@ -226,7 +264,7 @@ export function registerRoutinesRuntimeCommands(registry: CommandRegistry): void
|
|
|
226
264
|
name: 'routines',
|
|
227
265
|
aliases: ['routine'],
|
|
228
266
|
description: 'Manage local GoodVibes Agent routines',
|
|
229
|
-
usage: '[list|enabled|search <query>|show <id>|create --name <name> --description <summary> --steps <steps>|update <id> [--name ...] [--description ...] [--steps ...]|enable <id>|disable <id>|start <id>|review <id>|stale <id> <reason...>|delete <id> --yes]',
|
|
267
|
+
usage: '[list|enabled|search <query>|show <id>|create --name <name> --description <summary> --steps <steps>|update <id> [--name ...] [--description ...] [--steps ...]|enable <id>|disable <id>|start <id>|review <id>|stale <id> <reason...>|promote <id> --cron <expr> --yes|delete <id> --yes]',
|
|
230
268
|
handler: runRoutinesRuntimeCommand,
|
|
231
269
|
});
|
|
232
270
|
}
|