@pellux/goodvibes-agent 1.5.5 → 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/package/main.js +94130 -79335
- package/docs/tools-and-commands.md +2 -1
- package/package.json +6 -3
- package/src/agent/autonomy-schedule-format.ts +1 -1
- package/src/agent/autonomy-schedule.ts +3 -3
- package/src/agent/calendar/calendar-oauth-service.ts +324 -0
- package/src/agent/calendar-subscription-registry.ts +497 -0
- package/src/agent/email/email-service.ts +57 -0
- package/src/agent/email/smtp-client.ts +30 -10
- package/src/agent/memory-prompt.ts +143 -12
- package/src/agent/operator-actions.ts +26 -22
- package/src/agent/prompt-context-receipts.ts +45 -18
- package/src/agent/reminder-schedule-format.ts +1 -1
- package/src/agent/reminder-schedule.ts +3 -3
- package/src/agent/routine-schedule-format.ts +2 -2
- package/src/agent/routine-schedule-promotion.ts +5 -5
- package/src/agent/routine-schedule-receipts.ts +2 -2
- package/src/agent/schedule-edit-format.ts +1 -1
- package/src/agent/schedule-edit.ts +5 -5
- package/src/agent/session-registration.ts +281 -0
- package/src/agent/vibe-file.ts +50 -0
- package/src/cli/local-library-command-shared.ts +146 -0
- package/src/cli/management.ts +2 -1
- package/src/cli/personas-command.ts +252 -0
- package/src/cli/resume-relaunch-notice.ts +151 -0
- package/src/cli/routines-command.ts +25 -2
- package/src/cli/service-posture.ts +3 -2
- package/src/cli/skill-bundle-command.ts +175 -0
- package/src/cli/skills-command.ts +309 -0
- package/src/cli/status.ts +43 -7
- package/src/cli/tui-startup.ts +24 -1
- package/src/config/agent-settings-policy.ts +0 -1
- package/src/config/secret-config.ts +4 -0
- package/src/core/conversation-rendering.ts +20 -11
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/system-message-noise.ts +108 -0
- package/src/core/system-message-router.ts +54 -1
- package/src/core/thinking-overlay.ts +83 -0
- package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
- package/src/input/agent-workspace-access-command-editors.ts +108 -128
- package/src/input/agent-workspace-activation.ts +15 -0
- package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
- package/src/input/agent-workspace-basic-command-editors.ts +433 -564
- package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
- package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
- package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
- package/src/input/agent-workspace-categories.ts +7 -2
- package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
- package/src/input/agent-workspace-channel-command-editors.ts +42 -46
- package/src/input/agent-workspace-command-editor-engine.ts +133 -0
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
- package/src/input/agent-workspace-email-connect-editor.ts +162 -0
- package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
- package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
- package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
- package/src/input/agent-workspace-library-command-editors.ts +16 -2
- package/src/input/agent-workspace-live-counters.ts +55 -0
- package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
- package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
- package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
- package/src/input/agent-workspace-media-command-editors.ts +10 -8
- package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
- package/src/input/agent-workspace-memory-command-editors.ts +116 -141
- package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
- package/src/input/agent-workspace-operations-command-editors.ts +150 -162
- package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
- package/src/input/agent-workspace-provider-command-editors.ts +58 -68
- package/src/input/agent-workspace-search.ts +8 -1
- package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
- package/src/input/agent-workspace-session-command-editors.ts +160 -195
- package/src/input/agent-workspace-settings.ts +40 -1
- package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
- package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
- package/src/input/agent-workspace-snapshot-builders.ts +431 -0
- package/src/input/agent-workspace-snapshot-config.ts +43 -0
- package/src/input/agent-workspace-snapshot.ts +198 -432
- package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
- package/src/input/agent-workspace-task-command-editors.ts +27 -28
- package/src/input/agent-workspace-types.ts +35 -1
- package/src/input/agent-workspace.ts +5 -14
- package/src/input/command-registry.ts +23 -1
- package/src/input/commands/calendar-connect-runtime.ts +226 -0
- package/src/input/commands/calendar-runtime.ts +110 -7
- package/src/input/commands/calendar-subscription-runtime.ts +225 -0
- package/src/input/commands/email-runtime.ts +100 -40
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/network-scan-runtime.ts +75 -0
- package/src/input/commands/operator-actions-runtime.ts +9 -6
- package/src/input/commands/personas-runtime.ts +1 -1
- package/src/input/commands/schedule-runtime.ts +4 -4
- package/src/input/commands/session-content.ts +13 -1
- package/src/input/commands/session-workflow.ts +15 -20
- package/src/input/commands/session.ts +3 -1
- package/src/input/commands/shell-core.ts +20 -6
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +10 -0
- package/src/input/handler-feed.ts +87 -0
- package/src/input/handler-modal-routes.ts +6 -1
- package/src/input/handler.ts +5 -0
- package/src/input/panel-paste-flood-guard.ts +94 -0
- package/src/input/settings-modal-types.ts +5 -3
- package/src/input/settings-modal.ts +21 -0
- package/src/main.ts +36 -36
- package/src/permissions/approval-posture.ts +141 -0
- package/src/renderer/agent-workspace-context-lines.ts +10 -2
- package/src/renderer/compositor.ts +27 -4
- package/src/renderer/diff.ts +61 -18
- package/src/renderer/fullscreen-primitives.ts +37 -18
- package/src/renderer/markdown.ts +20 -10
- package/src/renderer/modal-factory.ts +25 -15
- package/src/renderer/overlay-box.ts +23 -12
- package/src/renderer/process-indicator.ts +8 -3
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +2 -0
- package/src/renderer/startup-theme-probe.ts +35 -0
- package/src/renderer/status-glyphs.ts +11 -15
- package/src/renderer/system-message.ts +17 -2
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/terminal-bg-probe.ts +373 -0
- package/src/renderer/terminal-escapes.ts +24 -0
- package/src/renderer/theme-mode-config.ts +87 -0
- package/src/renderer/theme.ts +241 -0
- package/src/renderer/thinking.ts +12 -3
- package/src/renderer/tool-call.ts +7 -3
- package/src/renderer/ui-factory.ts +92 -36
- package/src/renderer/ui-primitives.ts +33 -93
- package/src/runtime/bootstrap-core.ts +15 -0
- package/src/runtime/bootstrap-hook-bridge.ts +6 -0
- package/src/runtime/bootstrap-shell.ts +2 -0
- package/src/runtime/bootstrap.ts +68 -5
- package/src/runtime/calendar-boot-refresh.ts +105 -0
- package/src/runtime/lan-scan-consent.ts +253 -0
- package/src/runtime/services.ts +127 -2
- package/src/runtime/session-spine-rest-transport.ts +160 -0
- package/src/runtime/terminal-output-guard.ts +6 -1
- package/src/runtime/ui-services.ts +3 -0
- package/src/shell/agent-workspace-fullscreen.ts +5 -0
- package/src/shell/terminal-focus-mode.ts +120 -0
- package/src/shell/ui-openers.ts +13 -4
- package/src/tools/agent-harness-autonomy-intake.ts +5 -5
- package/src/tools/agent-harness-autonomy-queue.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +2 -1
- package/src/tools/agent-harness-metadata.ts +16 -8
- package/src/tools/agent-harness-operator-methods.ts +44 -8
- package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
- package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
- package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
- package/src/tools/agent-harness-personal-ops-types.ts +6 -0
- package/src/tools/agent-harness-prompt-context.ts +26 -12
- package/src/tools/agent-harness-workspace-actions.ts +4 -0
- package/src/tools/agent-local-registry-args.ts +117 -0
- package/src/tools/agent-local-registry-memory.ts +227 -0
- package/src/tools/agent-local-registry-tool.ts +19 -237
- package/src/tools/agent-operator-briefing-tool.ts +2 -2
- package/src/tools/agent-operator-method-tool.ts +13 -0
- package/src/tools/agent-policy-explanation.ts +39 -4
- package/src/tools/agent-schedule-tool.ts +5 -5
- package/src/utils/terminal-width.ts +98 -1
- package/src/version.ts +1 -1
- package/src/cli/local-library-command.ts +0 -825
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { discoverPersonas, type DiscoveredPersonaRecord } from '../agent/persona-discovery.ts';
|
|
2
|
+
import type { AgentPersonaRecord } from '../agent/persona-registry.ts';
|
|
3
|
+
import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../agent/record-labels.ts';
|
|
4
|
+
import type { CliCommandOutput } from './types.ts';
|
|
5
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
6
|
+
import {
|
|
7
|
+
csvOption,
|
|
8
|
+
errorOutput,
|
|
9
|
+
failure,
|
|
10
|
+
hasFlag,
|
|
11
|
+
optionValue,
|
|
12
|
+
parseOptions,
|
|
13
|
+
personaRegistry,
|
|
14
|
+
requiredOption,
|
|
15
|
+
shellPaths,
|
|
16
|
+
success,
|
|
17
|
+
} from './local-library-command-shared.ts';
|
|
18
|
+
|
|
19
|
+
function summarizePersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
|
|
20
|
+
const active = persona.id === activePersonaId ? 'active' : 'available';
|
|
21
|
+
const tags = persona.tags.length > 0 ? ` tags ${persona.tags.join(', ')}` : '';
|
|
22
|
+
return ` ${persona.id} ${active} ${formatAgentRecordReviewState(persona.reviewState)} ${persona.name} - ${persona.description}${tags}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderPersonaList(title: string, path: string, personas: readonly AgentPersonaRecord[], activePersonaId: string | null): string {
|
|
26
|
+
if (personas.length === 0) {
|
|
27
|
+
return [
|
|
28
|
+
title,
|
|
29
|
+
' No local Agent personas yet. Create one with: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>',
|
|
30
|
+
].join('\n');
|
|
31
|
+
}
|
|
32
|
+
return [
|
|
33
|
+
`${title} (${personas.length})`,
|
|
34
|
+
` store ${path}`,
|
|
35
|
+
...personas.map((persona) => summarizePersona(persona, activePersonaId)),
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderPersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
|
|
40
|
+
return [
|
|
41
|
+
`Persona ${persona.name}`,
|
|
42
|
+
` id ${persona.id}`,
|
|
43
|
+
` active: ${persona.id === activePersonaId ? 'yes' : 'no'}`,
|
|
44
|
+
` review: ${formatAgentRecordReviewState(persona.reviewState)}`,
|
|
45
|
+
` origin: ${formatAgentRecordOrigin(persona.source, persona.provenance)}`,
|
|
46
|
+
` tags: ${persona.tags.join(', ') || '(none)'}`,
|
|
47
|
+
` triggers: ${persona.triggers.join(', ') || '(manual)'}`,
|
|
48
|
+
` created: ${persona.createdAt}`,
|
|
49
|
+
` updated: ${persona.updatedAt}`,
|
|
50
|
+
persona.staleReason ? ` stale reason: ${persona.staleReason}` : '',
|
|
51
|
+
'',
|
|
52
|
+
persona.description,
|
|
53
|
+
'',
|
|
54
|
+
persona.body,
|
|
55
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function summarizeDiscoveredPersona(persona: DiscoveredPersonaRecord): string {
|
|
59
|
+
const description = persona.description ? ` - ${persona.description}` : '';
|
|
60
|
+
return [
|
|
61
|
+
` ${persona.name} ${persona.origin}${description}`,
|
|
62
|
+
` path ${persona.path}`,
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderDiscoveredPersonaList(personas: readonly DiscoveredPersonaRecord[]): string {
|
|
67
|
+
if (personas.length === 0) {
|
|
68
|
+
return [
|
|
69
|
+
'Discovered Agent persona files',
|
|
70
|
+
' No persona markdown files found in Agent persona folders.',
|
|
71
|
+
' Search roots: .goodvibes/personas, .goodvibes/agent/personas, ~/.goodvibes/personas, ~/.goodvibes/agent/personas',
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
return [
|
|
75
|
+
`Discovered Agent persona files (${personas.length})`,
|
|
76
|
+
...personas.map(summarizeDiscoveredPersona),
|
|
77
|
+
'',
|
|
78
|
+
'Import one with: goodvibes-agent personas import-discovered <name> --yes',
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function discoveredPersonaLookupValues(persona: DiscoveredPersonaRecord): readonly string[] {
|
|
83
|
+
const slug = persona.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
84
|
+
const basename = persona.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
|
|
85
|
+
return [persona.name, slug, persona.path, basename]
|
|
86
|
+
.map((value) => value.trim().toLowerCase())
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findDiscoveredPersona(personas: readonly DiscoveredPersonaRecord[], idOrName: string): DiscoveredPersonaRecord | null {
|
|
91
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
92
|
+
if (!lookup) return null;
|
|
93
|
+
return personas.find((persona) => discoveredPersonaLookupValues(persona).includes(lookup)) ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function discoveredPersonaFrontmatterList(persona: DiscoveredPersonaRecord, key: string): readonly string[] {
|
|
97
|
+
const value = persona.frontmatter[key];
|
|
98
|
+
if (!value) return [];
|
|
99
|
+
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function usagePersonas(): string {
|
|
103
|
+
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]';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function handlePersonasCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
107
|
+
try {
|
|
108
|
+
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
109
|
+
const normalized = sub.toLowerCase();
|
|
110
|
+
const registry = personaRegistry(runtime);
|
|
111
|
+
const snapshot = registry.snapshot();
|
|
112
|
+
if (normalized === 'list' || normalized === 'ls') {
|
|
113
|
+
return success(runtime, 'agent.personas.list', snapshot, renderPersonaList('Agent personas', snapshot.path, snapshot.personas, snapshot.activePersonaId));
|
|
114
|
+
}
|
|
115
|
+
if (normalized === 'active') {
|
|
116
|
+
const active = snapshot.activePersona;
|
|
117
|
+
if (!active) return failure(runtime, 'agent.personas.active_missing', 'No active Agent persona.', 1);
|
|
118
|
+
return success(runtime, 'agent.personas.active', active, renderPersona(active, active.id));
|
|
119
|
+
}
|
|
120
|
+
if (normalized === 'discover') {
|
|
121
|
+
const discovered = await discoverPersonas(shellPaths(runtime));
|
|
122
|
+
return success(runtime, 'agent.personas.discover', { personas: discovered }, renderDiscoveredPersonaList(discovered));
|
|
123
|
+
}
|
|
124
|
+
if (normalized === 'import-discovered' || normalized === 'import-persona') {
|
|
125
|
+
const options = parseOptions(rest);
|
|
126
|
+
const name = options.positionals.join(' ').trim();
|
|
127
|
+
if (!name) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas import-discovered <name> [--use] --yes', 2);
|
|
128
|
+
const discovered = findDiscoveredPersona(await discoverPersonas(shellPaths(runtime)), name);
|
|
129
|
+
if (!discovered) {
|
|
130
|
+
return failure(runtime, 'persona_discovery_not_found', `Unknown discovered Agent persona ${name}\nRun goodvibes-agent personas discover to inspect available persona files.`, 1);
|
|
131
|
+
}
|
|
132
|
+
if (!hasFlag(options, 'yes')) {
|
|
133
|
+
return success(runtime, 'agent.personas.import_discovered.preview', { persona: discovered }, [
|
|
134
|
+
'Agent persona import preview',
|
|
135
|
+
` name ${discovered.name}`,
|
|
136
|
+
` origin ${discovered.origin}`,
|
|
137
|
+
` path ${discovered.path}`,
|
|
138
|
+
` description ${discovered.description || '(none)'}`,
|
|
139
|
+
` body characters ${discovered.body.length}`,
|
|
140
|
+
' next rerun with --yes to import into the Agent-local persona registry',
|
|
141
|
+
].join('\n'));
|
|
142
|
+
}
|
|
143
|
+
const persona = registry.create({
|
|
144
|
+
name: discovered.name,
|
|
145
|
+
description: discovered.description || `Imported persona from ${discovered.origin} markdown file.`,
|
|
146
|
+
body: discovered.body,
|
|
147
|
+
tags: discoveredPersonaFrontmatterList(discovered, 'tags'),
|
|
148
|
+
triggers: discoveredPersonaFrontmatterList(discovered, 'triggers'),
|
|
149
|
+
source: 'imported',
|
|
150
|
+
provenance: `discovered:${discovered.origin}:${discovered.path}`,
|
|
151
|
+
});
|
|
152
|
+
if (hasFlag(options, 'use')) registry.setActive(persona.id);
|
|
153
|
+
return success(runtime, 'agent.personas.import_discovered', persona, [
|
|
154
|
+
`Imported Agent persona ${persona.id}: ${persona.name}${hasFlag(options, 'use') ? ' (active)' : ''}`,
|
|
155
|
+
` name ${persona.name}`,
|
|
156
|
+
` active ${hasFlag(options, 'use') ? 'yes' : 'no'}`,
|
|
157
|
+
].join('\n'));
|
|
158
|
+
}
|
|
159
|
+
if (normalized === 'search' || normalized === 'find') {
|
|
160
|
+
const query = rest.join(' ').trim();
|
|
161
|
+
const results = registry.search(query);
|
|
162
|
+
return success(runtime, 'agent.personas.search', { query, results }, renderPersonaList(`Agent personas matching "${query}"`, snapshot.path, results, snapshot.activePersonaId));
|
|
163
|
+
}
|
|
164
|
+
if (normalized === 'show' || normalized === 'get') {
|
|
165
|
+
const id = rest[0];
|
|
166
|
+
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas show <id>', 2);
|
|
167
|
+
const persona = registry.get(id);
|
|
168
|
+
if (!persona) return failure(runtime, 'persona_not_found', `Unknown Agent persona ${id}`, 1);
|
|
169
|
+
return success(runtime, 'agent.personas.show', persona, renderPersona(persona, snapshot.activePersonaId));
|
|
170
|
+
}
|
|
171
|
+
if (normalized === 'create') {
|
|
172
|
+
const options = parseOptions(rest);
|
|
173
|
+
const persona = registry.create({
|
|
174
|
+
name: requiredOption(options, 'name', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
|
|
175
|
+
description: requiredOption(options, 'description', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
|
|
176
|
+
body: requiredOption(options, 'body', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
|
|
177
|
+
tags: csvOption(options, 'tags'),
|
|
178
|
+
triggers: csvOption(options, 'triggers'),
|
|
179
|
+
provenance: optionValue(options, 'provenance') ?? 'Command',
|
|
180
|
+
});
|
|
181
|
+
if (hasFlag(options, 'use')) registry.setActive(persona.id);
|
|
182
|
+
return success(runtime, 'agent.personas.create', persona, [
|
|
183
|
+
'Agent persona created',
|
|
184
|
+
` id ${persona.id}`,
|
|
185
|
+
hasFlag(options, 'use') ? ' (active)' : '',
|
|
186
|
+
` active ${hasFlag(options, 'use') ? 'yes' : 'no'}`,
|
|
187
|
+
].filter(Boolean).join('\n'));
|
|
188
|
+
}
|
|
189
|
+
if (normalized === 'update') {
|
|
190
|
+
const id = rest[0];
|
|
191
|
+
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas update <id> [--name ...] [--description ...] [--body ...]', 2);
|
|
192
|
+
const options = parseOptions(rest.slice(1));
|
|
193
|
+
const persona = registry.update(id, {
|
|
194
|
+
name: optionValue(options, 'name'),
|
|
195
|
+
description: optionValue(options, 'description'),
|
|
196
|
+
body: optionValue(options, 'body'),
|
|
197
|
+
tags: csvOption(options, 'tags'),
|
|
198
|
+
triggers: csvOption(options, 'triggers'),
|
|
199
|
+
provenance: optionValue(options, 'provenance'),
|
|
200
|
+
});
|
|
201
|
+
return success(runtime, 'agent.personas.update', persona, [
|
|
202
|
+
'Agent persona updated',
|
|
203
|
+
` id ${persona.id}`,
|
|
204
|
+
].join('\n'));
|
|
205
|
+
}
|
|
206
|
+
if (normalized === 'use') {
|
|
207
|
+
const id = rest[0];
|
|
208
|
+
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas use <id>', 2);
|
|
209
|
+
const persona = registry.setActive(id);
|
|
210
|
+
return success(runtime, 'agent.personas.use', persona, [
|
|
211
|
+
'Active Agent persona',
|
|
212
|
+
` id ${persona.id}`,
|
|
213
|
+
].join('\n'));
|
|
214
|
+
}
|
|
215
|
+
if (normalized === 'clear') {
|
|
216
|
+
registry.clearActive();
|
|
217
|
+
return success(runtime, 'agent.personas.clear', { activePersonaId: null }, 'Active Agent persona cleared.');
|
|
218
|
+
}
|
|
219
|
+
if (normalized === 'review') {
|
|
220
|
+
const id = rest[0];
|
|
221
|
+
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas review <id>', 2);
|
|
222
|
+
const persona = registry.markReviewed(id);
|
|
223
|
+
return success(runtime, 'agent.personas.review', persona, [
|
|
224
|
+
'Agent persona reviewed',
|
|
225
|
+
` id ${persona.id}`,
|
|
226
|
+
].join('\n'));
|
|
227
|
+
}
|
|
228
|
+
if (normalized === 'stale') {
|
|
229
|
+
const id = rest[0];
|
|
230
|
+
if (!id || rest.length < 2) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas stale <id> <reason>', 2);
|
|
231
|
+
const persona = registry.markStale(id, rest.slice(1).join(' '));
|
|
232
|
+
return success(runtime, 'agent.personas.stale', persona, [
|
|
233
|
+
'Agent persona marked stale',
|
|
234
|
+
` id ${persona.id}`,
|
|
235
|
+
].join('\n'));
|
|
236
|
+
}
|
|
237
|
+
if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
|
|
238
|
+
const options = parseOptions(rest);
|
|
239
|
+
const id = options.positionals[0];
|
|
240
|
+
if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas delete <id> --yes', 2);
|
|
241
|
+
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent persona ${id} without --yes.`, 2);
|
|
242
|
+
const persona = registry.deletePersona(id);
|
|
243
|
+
return success(runtime, 'agent.personas.delete', persona, [
|
|
244
|
+
`Agent persona deleted: ${id}`,
|
|
245
|
+
` id ${persona.id}`,
|
|
246
|
+
].join('\n'));
|
|
247
|
+
}
|
|
248
|
+
return failure(runtime, 'invalid_persona_command', usagePersonas(), 2);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return errorOutput(runtime, error, 'agent.personas.error');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RESUME-ON-RELAUNCH (W4-A4).
|
|
5
|
+
*
|
|
6
|
+
* The dogfood finding: relaunching the agent after a normal (non-crash) exit
|
|
7
|
+
* silently starts fresh with no offer or notice about the prior session — the
|
|
8
|
+
* only way back was knowing the exact `goodvibes-agent sessions resume <id>`
|
|
9
|
+
* command, or digging into `/health continuity`. This module builds an
|
|
10
|
+
* honest, one-line boot notice that surfaces the resume affordance without
|
|
11
|
+
* ever auto-resuming or blocking startup.
|
|
12
|
+
*
|
|
13
|
+
* Honesty rules (never claim restorability the app can't deliver):
|
|
14
|
+
* - no last-session pointer on disk -> no notice (clean first run)
|
|
15
|
+
* - a pointer exists but the session it names is missing/unreadable (stale
|
|
16
|
+
* or dangling) -> say so plainly and point at `/session list`, never a
|
|
17
|
+
* resume action that would dead-end
|
|
18
|
+
* - a pointer exists and the session resolves -> an actionable one-liner
|
|
19
|
+
* naming the EXISTING `/session resume <id>` path (this is surfacing, not
|
|
20
|
+
* new resume logic)
|
|
21
|
+
* - never auto-resume: the user always chooses, and declining is
|
|
22
|
+
* frictionless (just start typing — the saved session stays put)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Minimal session facts needed to render an honest resume notice. */
|
|
26
|
+
export interface ResumableSessionSummary {
|
|
27
|
+
readonly sessionId: string;
|
|
28
|
+
readonly title: string;
|
|
29
|
+
readonly timestamp: number;
|
|
30
|
+
readonly messageCount: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResumeRelaunchNoticeInput {
|
|
34
|
+
/** The session id recorded in the last-session pointer file, or null when no pointer exists. */
|
|
35
|
+
readonly pointerSessionId: string | null;
|
|
36
|
+
/**
|
|
37
|
+
* The resolved session the pointer refers to, or null when the pointer
|
|
38
|
+
* exists but the session it names could not be found/read (stale or
|
|
39
|
+
* dangling pointer).
|
|
40
|
+
*/
|
|
41
|
+
readonly session: ResumableSessionSummary | null;
|
|
42
|
+
readonly now?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Formats an elapsed duration the way a human would describe "how long ago".
|
|
47
|
+
*
|
|
48
|
+
* F4 fix: this used Math.round at every tier, which rounds UP near a
|
|
49
|
+
* boundary — a session from 45 minutes ago (2,700,000ms) rounded to "1h ago"
|
|
50
|
+
* (round(45/60) = 1), which reads as roughly twice as stale as it really is.
|
|
51
|
+
* Every tier below now floors instead, so a duration is never described as
|
|
52
|
+
* further in the past than it actually is: 45m through 59m59s all read as
|
|
53
|
+
* "Nm ago", and the "1h" label only appears once a full hour has actually
|
|
54
|
+
* elapsed. Minutes are floored to at least 1 once past the 45s "moments"
|
|
55
|
+
* cutoff, so a duration just past that cutoff never reads as "0m ago".
|
|
56
|
+
*/
|
|
57
|
+
export function formatRelaunchAge(elapsedMs: number): string {
|
|
58
|
+
const elapsed = Math.max(0, elapsedMs);
|
|
59
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
60
|
+
if (seconds < 45) return 'moments';
|
|
61
|
+
const minutes = Math.max(1, Math.floor(seconds / 60));
|
|
62
|
+
if (minutes < 60) return `${minutes}m`;
|
|
63
|
+
const hours = Math.floor(minutes / 60);
|
|
64
|
+
if (hours < 36) return `${hours}h`;
|
|
65
|
+
const days = Math.floor(hours / 24);
|
|
66
|
+
return `${days}d`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Builds the boot-time resume notice text, or null when nothing should be
|
|
71
|
+
* shown (no pointer at all — a clean first run, or the caller already routed
|
|
72
|
+
* an explicit resume/onboarding command elsewhere).
|
|
73
|
+
*/
|
|
74
|
+
export function buildResumeRelaunchNotice(input: ResumeRelaunchNoticeInput): string | null {
|
|
75
|
+
if (!input.pointerSessionId) return null;
|
|
76
|
+
|
|
77
|
+
if (!input.session) {
|
|
78
|
+
return [
|
|
79
|
+
'[Resume] Your last session pointer is unavailable (the saved session may have been moved, renamed, or deleted).',
|
|
80
|
+
' Starting fresh. Run /session list to see any sessions that are still on disk.',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// F5: a 0-message session has nothing to resume into — offering a resume
|
|
85
|
+
// action for it is a dead-end same as a stale pointer, but less honest,
|
|
86
|
+
// since the session itself does resolve. Stay silent, exactly like the
|
|
87
|
+
// no-pointer (clean first run) case above, rather than dangle a resume
|
|
88
|
+
// command in front of an empty conversation.
|
|
89
|
+
if (input.session.messageCount <= 0) return null;
|
|
90
|
+
|
|
91
|
+
const { session } = input;
|
|
92
|
+
const age = formatRelaunchAge((input.now ?? Date.now()) - session.timestamp);
|
|
93
|
+
const label = session.title.trim() || session.sessionId;
|
|
94
|
+
const messageWord = session.messageCount === 1 ? 'message' : 'messages';
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
`[Resume] Your last session is available: "${label}" — ${age} ago, ${session.messageCount} ${messageWord}.`,
|
|
98
|
+
` Resume it: /session resume ${session.sessionId}`,
|
|
99
|
+
' Or just start typing to begin a new session — the saved one stays put.',
|
|
100
|
+
].join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolves the pointer session id to its display facts via the existing
|
|
105
|
+
* SessionManager, honestly returning null when the session is missing or
|
|
106
|
+
* unreadable rather than throwing.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveResumableSession(
|
|
109
|
+
sessionManager: Pick<SessionManager, 'list'>,
|
|
110
|
+
pointerSessionId: string,
|
|
111
|
+
): ResumableSessionSummary | null {
|
|
112
|
+
const found = sessionManager.list().find((session) => session.name === pointerSessionId);
|
|
113
|
+
if (!found) return null;
|
|
114
|
+
return {
|
|
115
|
+
sessionId: found.name,
|
|
116
|
+
title: found.title,
|
|
117
|
+
timestamp: found.timestamp,
|
|
118
|
+
messageCount: found.messageCount,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface SurfaceResumeRelaunchNoticeDeps {
|
|
123
|
+
readonly getLastSessionPointer: () => string | null;
|
|
124
|
+
/** True when a crash-recovery prompt is already showing (that flow owns its own restore path and takes priority). */
|
|
125
|
+
readonly isRecoveryPending: () => boolean;
|
|
126
|
+
readonly findSession: (sessionId: string) => ResumableSessionSummary | null;
|
|
127
|
+
readonly print: (text: string) => void;
|
|
128
|
+
readonly now?: () => number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Decides whether to surface the resume notice and prints it. Never throws,
|
|
133
|
+
* never blocks startup, and never auto-resumes — the user always chooses by
|
|
134
|
+
* running the printed command or by just typing to start fresh.
|
|
135
|
+
*/
|
|
136
|
+
export function surfaceResumeRelaunchNotice(deps: SurfaceResumeRelaunchNoticeDeps): void {
|
|
137
|
+
try {
|
|
138
|
+
if (deps.isRecoveryPending()) return;
|
|
139
|
+
const pointerSessionId = deps.getLastSessionPointer();
|
|
140
|
+
if (!pointerSessionId) return;
|
|
141
|
+
const session = deps.findSession(pointerSessionId);
|
|
142
|
+
const notice = buildResumeRelaunchNotice({
|
|
143
|
+
pointerSessionId,
|
|
144
|
+
session,
|
|
145
|
+
now: deps.now ? deps.now() : Date.now(),
|
|
146
|
+
});
|
|
147
|
+
if (notice) deps.print(notice);
|
|
148
|
+
} catch {
|
|
149
|
+
// Never block startup on a resume-notice failure.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -290,7 +290,7 @@ async function handleRoutinePromotion(runtime: CliCommandRuntime, args: readonly
|
|
|
290
290
|
if (!parsed.yes) {
|
|
291
291
|
const value: RoutinesCommandSuccess<typeof preview> = {
|
|
292
292
|
ok: true,
|
|
293
|
-
kind: 'schedules.create.preview',
|
|
293
|
+
kind: 'automation.schedules.create.preview',
|
|
294
294
|
data: preview,
|
|
295
295
|
};
|
|
296
296
|
return {
|
|
@@ -496,6 +496,29 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
|
|
|
496
496
|
exitCode: 0,
|
|
497
497
|
};
|
|
498
498
|
}
|
|
499
|
+
if (normalized === 'delete' || normalized === 'remove') {
|
|
500
|
+
const options = parseRoutineOptions(rest);
|
|
501
|
+
const id = options.positionals[0];
|
|
502
|
+
if (!id) return { output: 'Usage: goodvibes-agent routines delete <id> --yes', exitCode: 2 };
|
|
503
|
+
if (!options.yes) {
|
|
504
|
+
return { output: `Refusing to delete Agent routine ${id} without --yes.`, exitCode: 2 };
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const routine = registry.deleteRoutine(id);
|
|
508
|
+
const value: RoutinesCommandSuccess<AgentRoutineRecord> = { ok: true, kind: 'agent.routines.delete', data: routine };
|
|
509
|
+
return {
|
|
510
|
+
output: jsonOrText(runtime, value, `Deleted Agent routine ${routine.id}: ${routine.name}`),
|
|
511
|
+
exitCode: 0,
|
|
512
|
+
};
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return commandFailure(
|
|
515
|
+
runtime,
|
|
516
|
+
'routine_not_found',
|
|
517
|
+
error instanceof Error ? error.message : String(error),
|
|
518
|
+
1,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
499
522
|
if (normalized === 'receipts' || normalized === 'history') {
|
|
500
523
|
const snapshot = routineReceiptStore(runtime).snapshot();
|
|
501
524
|
const value: RoutinesCommandSuccess<typeof snapshot> = {
|
|
@@ -544,7 +567,7 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
|
|
|
544
567
|
return handleRoutinePromotion(runtime, rest);
|
|
545
568
|
}
|
|
546
569
|
return {
|
|
547
|
-
output: `Usage: goodvibes-agent routines [list|enabled|attention|discover|import-discovered <name> --yes|create --name <name> --description <summary> --steps <steps>|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]`,
|
|
570
|
+
output: `Usage: goodvibes-agent routines [list|enabled|attention|discover|import-discovered <name> --yes|create --name <name> --description <summary> --steps <steps>|show <id>|delete <id> --yes|receipts|reconcile|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--delivery-channel <channel>|--delivery-route <route>|--delivery-webhook <url>] --yes]`,
|
|
548
571
|
exitCode: 2,
|
|
549
572
|
};
|
|
550
573
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readSync, statSync } from 'node:fs';
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
import { resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
|
|
4
5
|
import type { ConfigKey, ConfigManager } from '../config/index.ts';
|
|
5
6
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
6
7
|
import type { RuntimeEndpointBinding, RuntimeEndpointId } from './endpoints.ts';
|
|
@@ -178,7 +179,7 @@ export async function buildCliServicePosture(
|
|
|
178
179
|
enabled: runtime.configManager.get('service.enabled') === true,
|
|
179
180
|
autostart: runtime.configManager.get('service.autostart') === true,
|
|
180
181
|
restartOnFailure: runtime.configManager.get('service.restartOnFailure') === true,
|
|
181
|
-
daemonEnabled: runtime.configManager
|
|
182
|
+
daemonEnabled: resolveDaemonEnabled(runtime.configManager),
|
|
182
183
|
};
|
|
183
184
|
const serverBackedEnabled = config.daemonEnabled || endpoints.some((endpoint) => endpoint.enabled);
|
|
184
185
|
const issues: string[] = [];
|
|
@@ -228,7 +229,7 @@ export function formatCliServicePosture(posture: CliServicePosture, json = false
|
|
|
228
229
|
' Agent starts connected host: no',
|
|
229
230
|
` external host config present: ${yesNo(posture.config.enabled)}`,
|
|
230
231
|
' external host lifecycle config: ignored by Agent',
|
|
231
|
-
`
|
|
232
|
+
` daemon considered enabled: ${yesNo(posture.config.daemonEnabled)}`,
|
|
232
233
|
` log: ${posture.log.path ?? 'n/a'} (${posture.log.exists ? 'present' : 'missing'})`,
|
|
233
234
|
...(posture.log.readError ? [` log read error: ${posture.log.readError}`] : []),
|
|
234
235
|
'',
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../agent/record-labels.ts';
|
|
2
|
+
import {
|
|
3
|
+
evaluateAgentSkillBundleReadiness,
|
|
4
|
+
formatAgentSkillRequirement,
|
|
5
|
+
type AgentSkillBundleRecord,
|
|
6
|
+
type AgentSkillRecord,
|
|
7
|
+
} from '../agent/skill-registry.ts';
|
|
8
|
+
import type { CliCommandOutput } from './types.ts';
|
|
9
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
10
|
+
import {
|
|
11
|
+
csvOption,
|
|
12
|
+
errorOutput,
|
|
13
|
+
failure,
|
|
14
|
+
hasFlag,
|
|
15
|
+
optionValue,
|
|
16
|
+
parseOptions,
|
|
17
|
+
requiredOption,
|
|
18
|
+
skillRegistry,
|
|
19
|
+
success,
|
|
20
|
+
} from './local-library-command-shared.ts';
|
|
21
|
+
|
|
22
|
+
function summarizeBundle(bundle: AgentSkillBundleRecord, skills: readonly AgentSkillRecord[]): string {
|
|
23
|
+
const enabled = bundle.enabled ? 'enabled' : 'disabled';
|
|
24
|
+
const readiness = evaluateAgentSkillBundleReadiness(bundle, skills);
|
|
25
|
+
const missing = [
|
|
26
|
+
...readiness.missingRequirements.map(formatAgentSkillRequirement),
|
|
27
|
+
...readiness.missingSkillIds.map((skillId) => `skill:${skillId}`),
|
|
28
|
+
];
|
|
29
|
+
const ready = readiness.ready ? 'ready' : `needs ${missing.join(',')}`;
|
|
30
|
+
return ` ${bundle.id} ${enabled} ${formatAgentRecordReviewState(bundle.reviewState)} ${ready} ${bundle.name} - ${bundle.description} skills ${bundle.skillIds.join(',')}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function renderBundleList(title: string, path: string, bundles: readonly AgentSkillBundleRecord[], skills: readonly AgentSkillRecord[], emptyMessage?: string): string {
|
|
34
|
+
if (bundles.length === 0) {
|
|
35
|
+
return [
|
|
36
|
+
title,
|
|
37
|
+
` ${emptyMessage ?? 'No Agent-local skill bundles yet.'}`,
|
|
38
|
+
emptyMessage ? '' : ' Create one with: goodvibes-agent skills bundle create --name <name> --description <summary> --skills <id,id>',
|
|
39
|
+
].filter(Boolean).join('\n');
|
|
40
|
+
}
|
|
41
|
+
return [
|
|
42
|
+
`${title} (${bundles.length})`,
|
|
43
|
+
` store ${path}`,
|
|
44
|
+
...bundles.map((bundle) => summarizeBundle(bundle, skills)),
|
|
45
|
+
].join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderBundle(bundle: AgentSkillBundleRecord, skills: readonly AgentSkillRecord[]): string {
|
|
49
|
+
const readiness = evaluateAgentSkillBundleReadiness(bundle, skills);
|
|
50
|
+
const missing = [
|
|
51
|
+
...readiness.missingRequirements.map(formatAgentSkillRequirement),
|
|
52
|
+
...readiness.missingSkillIds.map((skillId) => `skill:${skillId}`),
|
|
53
|
+
];
|
|
54
|
+
return [
|
|
55
|
+
`Skill bundle ${bundle.name}`,
|
|
56
|
+
` id ${bundle.id}`,
|
|
57
|
+
` enabled: ${bundle.enabled ? 'yes' : 'no'}`,
|
|
58
|
+
` readiness: ${readiness.ready ? 'ready' : 'needs setup'}`,
|
|
59
|
+
missing.length > 0 ? ` missing: ${missing.join(', ')}` : '',
|
|
60
|
+
` review: ${formatAgentRecordReviewState(bundle.reviewState)}`,
|
|
61
|
+
` origin: ${formatAgentRecordOrigin(bundle.source, bundle.provenance)}`,
|
|
62
|
+
` skills: ${bundle.skillIds.join(', ')}`,
|
|
63
|
+
` created: ${bundle.createdAt}`,
|
|
64
|
+
` updated: ${bundle.updatedAt}`,
|
|
65
|
+
bundle.staleReason ? ` stale reason: ${bundle.staleReason}` : '',
|
|
66
|
+
'',
|
|
67
|
+
bundle.description,
|
|
68
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function usageBundles(): string {
|
|
72
|
+
return 'Usage: goodvibes-agent skills bundle [list|enabled|attention|search <query>|show <id>|create|update <id>|enable <id>|disable <id>|review <id>|stale <id> <reason>|delete <id> --yes]';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function handleSkillBundleCommand(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
76
|
+
try {
|
|
77
|
+
const [sub = 'list', ...rest] = args;
|
|
78
|
+
const normalized = sub.toLowerCase();
|
|
79
|
+
const registry = skillRegistry(runtime);
|
|
80
|
+
const snapshot = registry.snapshot();
|
|
81
|
+
if (normalized === 'list' || normalized === 'ls') {
|
|
82
|
+
return success(runtime, 'agent.skills.bundles.list', { path: snapshot.path, bundles: snapshot.bundles }, renderBundleList('Agent skill bundles', snapshot.path, snapshot.bundles, snapshot.skills));
|
|
83
|
+
}
|
|
84
|
+
if (normalized === 'enabled') {
|
|
85
|
+
return success(runtime, 'agent.skills.bundles.enabled', { path: snapshot.path, bundles: snapshot.enabledBundles }, renderBundleList('Enabled Agent skill bundles', snapshot.path, snapshot.enabledBundles, snapshot.skills));
|
|
86
|
+
}
|
|
87
|
+
if (normalized === 'attention' || normalized === 'needs-setup') {
|
|
88
|
+
const bundles = snapshot.bundles.filter((bundle) => !evaluateAgentSkillBundleReadiness(bundle, snapshot.skills).ready);
|
|
89
|
+
return success(runtime, 'agent.skills.bundles.attention', { path: snapshot.path, bundles }, renderBundleList('Agent skill bundles needing setup', snapshot.path, bundles, snapshot.skills, 'No Agent-local skill bundles need setup.'));
|
|
90
|
+
}
|
|
91
|
+
if (normalized === 'search' || normalized === 'find') {
|
|
92
|
+
const query = rest.join(' ').trim();
|
|
93
|
+
const results = registry.searchBundles(query);
|
|
94
|
+
return success(runtime, 'agent.skills.bundles.search', { query, results }, renderBundleList(`Agent skill bundles matching "${query}"`, snapshot.path, results, snapshot.skills));
|
|
95
|
+
}
|
|
96
|
+
if (normalized === 'show' || normalized === 'get') {
|
|
97
|
+
const id = rest[0];
|
|
98
|
+
if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle show <id>', 2);
|
|
99
|
+
const bundle = registry.getBundle(id);
|
|
100
|
+
if (!bundle) return failure(runtime, 'skill_bundle_not_found', `Unknown Agent skill bundle ${id}`, 1);
|
|
101
|
+
return success(runtime, 'agent.skills.bundles.show', bundle, renderBundle(bundle, snapshot.skills));
|
|
102
|
+
}
|
|
103
|
+
if (normalized === 'create') {
|
|
104
|
+
const options = parseOptions(rest);
|
|
105
|
+
const usage = 'Usage: goodvibes-agent skills bundle create --name <name> --description <summary> --skills <id,id>';
|
|
106
|
+
const bundle = registry.createBundle({
|
|
107
|
+
name: requiredOption(options, 'name', usage),
|
|
108
|
+
description: requiredOption(options, 'description', usage),
|
|
109
|
+
skillIds: requiredOption(options, 'skills', usage).split(',').map((entry) => entry.trim()).filter(Boolean),
|
|
110
|
+
enabled: hasFlag(options, 'enabled'),
|
|
111
|
+
provenance: optionValue(options, 'provenance') ?? 'Command',
|
|
112
|
+
});
|
|
113
|
+
return success(runtime, 'agent.skills.bundles.create', bundle, [
|
|
114
|
+
'Agent skill bundle created',
|
|
115
|
+
` id ${bundle.id}`,
|
|
116
|
+
].join('\n'));
|
|
117
|
+
}
|
|
118
|
+
if (normalized === 'update') {
|
|
119
|
+
const id = rest[0];
|
|
120
|
+
if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle update <id> [--name ...] [--description ...] [--skills id,id]', 2);
|
|
121
|
+
const options = parseOptions(rest.slice(1));
|
|
122
|
+
const bundle = registry.updateBundle(id, {
|
|
123
|
+
name: optionValue(options, 'name'),
|
|
124
|
+
description: optionValue(options, 'description'),
|
|
125
|
+
skillIds: csvOption(options, 'skills'),
|
|
126
|
+
provenance: optionValue(options, 'provenance'),
|
|
127
|
+
});
|
|
128
|
+
return success(runtime, 'agent.skills.bundles.update', bundle, [
|
|
129
|
+
'Agent skill bundle updated',
|
|
130
|
+
` id ${bundle.id}`,
|
|
131
|
+
].join('\n'));
|
|
132
|
+
}
|
|
133
|
+
if (normalized === 'enable' || normalized === 'disable') {
|
|
134
|
+
const id = rest[0];
|
|
135
|
+
if (!id) return failure(runtime, 'invalid_skill_bundle_command', `Usage: goodvibes-agent skills bundle ${normalized} <id>`, 2);
|
|
136
|
+
const bundle = registry.setBundleEnabled(id, normalized === 'enable');
|
|
137
|
+
return success(runtime, `agent.skills.bundles.${normalized}`, bundle, [
|
|
138
|
+
`Agent skill bundle ${normalized === 'enable' ? 'enabled' : 'disabled'}`,
|
|
139
|
+
` id ${bundle.id}`,
|
|
140
|
+
].join('\n'));
|
|
141
|
+
}
|
|
142
|
+
if (normalized === 'review') {
|
|
143
|
+
const id = rest[0];
|
|
144
|
+
if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle review <id>', 2);
|
|
145
|
+
const bundle = registry.markBundleReviewed(id);
|
|
146
|
+
return success(runtime, 'agent.skills.bundles.review', bundle, [
|
|
147
|
+
'Agent skill bundle reviewed',
|
|
148
|
+
` id ${bundle.id}`,
|
|
149
|
+
].join('\n'));
|
|
150
|
+
}
|
|
151
|
+
if (normalized === 'stale') {
|
|
152
|
+
const id = rest[0];
|
|
153
|
+
if (!id || rest.length < 2) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle stale <id> <reason>', 2);
|
|
154
|
+
const bundle = registry.markBundleStale(id, rest.slice(1).join(' '));
|
|
155
|
+
return success(runtime, 'agent.skills.bundles.stale', bundle, [
|
|
156
|
+
'Agent skill bundle marked stale',
|
|
157
|
+
` id ${bundle.id}`,
|
|
158
|
+
].join('\n'));
|
|
159
|
+
}
|
|
160
|
+
if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
|
|
161
|
+
const options = parseOptions(rest);
|
|
162
|
+
const id = options.positionals[0];
|
|
163
|
+
if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle delete <id> --yes', 2);
|
|
164
|
+
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent skill bundle ${id} without --yes.`, 2);
|
|
165
|
+
const bundle = registry.deleteBundle(id);
|
|
166
|
+
return success(runtime, 'agent.skills.bundles.delete', bundle, [
|
|
167
|
+
'Agent skill bundle deleted',
|
|
168
|
+
` id ${bundle.id}`,
|
|
169
|
+
].join('\n'));
|
|
170
|
+
}
|
|
171
|
+
return failure(runtime, 'invalid_skill_bundle_command', usageBundles(), 2);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return errorOutput(runtime, error, 'agent.skills.bundles.error');
|
|
174
|
+
}
|
|
175
|
+
}
|