@pellux/goodvibes-agent 0.1.1 → 0.1.3
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 +14 -0
- package/README.md +12 -1
- package/docs/README.md +2 -0
- package/docs/getting-started.md +19 -1
- package/docs/release-and-publishing.md +3 -1
- package/package.json +10 -1
- package/src/agent/persona-registry.ts +379 -0
- package/src/agent/skill-registry.ts +360 -0
- package/src/audio/spoken-turn-model-routing.ts +2 -1
- package/src/cli/agent-knowledge-command.ts +525 -0
- package/src/cli/help.ts +35 -0
- package/src/cli/management-commands.ts +3 -1
- package/src/cli/management.ts +33 -9
- package/src/cli/parser.ts +7 -0
- package/src/cli/types.ts +3 -0
- package/src/config/surface.ts +1 -0
- package/src/input/agent-workspace.ts +33 -3
- package/src/input/command-registry.ts +4 -1
- package/src/input/commands/agent-skills-runtime.ts +216 -0
- package/src/input/commands/delegation-runtime.ts +129 -0
- package/src/input/commands/knowledge.ts +18 -18
- package/src/input/commands/personas-runtime.ts +219 -0
- package/src/input/commands/shell-core.ts +9 -6
- package/src/input/commands/skills-runtime.ts +7 -2
- package/src/input/commands.ts +6 -0
- package/src/input/panel-integration-actions.ts +0 -52
- package/src/input/submission-router.ts +1 -1
- package/src/main.ts +2 -1
- package/src/panels/builtin/agent.ts +0 -14
- package/src/panels/builtin/session.ts +4 -3
- package/src/panels/index.ts +0 -5
- package/src/panels/orchestration-panel.ts +4 -5
- package/src/panels/qr-panel.ts +3 -2
- package/src/panels/tasks-panel.ts +4 -4
- package/src/renderer/agent-workspace.ts +2 -0
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +6 -2
- package/src/runtime/bootstrap-core.ts +8 -4
- package/src/runtime/bootstrap-shell.ts +5 -2
- package/src/runtime/bootstrap.ts +10 -2
- package/src/runtime/cloudflare-control-plane.ts +2 -1
- package/src/version.ts +1 -1
- package/src/daemon/cli.ts +0 -55
- package/src/daemon/safe-serve.ts +0 -61
- package/src/panels/diff-panel.ts +0 -520
- package/src/panels/file-explorer-panel.ts +0 -584
- package/src/panels/file-preview-panel.ts +0 -434
- package/src/panels/git-panel.ts +0 -638
- package/src/panels/sandbox-panel.ts +0 -283
- package/src/panels/symbol-outline-panel.ts +0 -486
- package/src/panels/worktree-panel.ts +0 -182
- package/src/panels/wrfc-panel.ts +0 -609
package/src/cli/management.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibe
|
|
|
21
21
|
import { inspectProviderAuth } from '@/runtime/index.ts';
|
|
22
22
|
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
23
23
|
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
24
|
+
import { UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
24
25
|
import type { GoodVibesCliParseResult } from './types.ts';
|
|
25
26
|
import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
|
|
26
27
|
import { classifyProviderSetup } from './provider-classification.ts';
|
|
@@ -31,6 +32,7 @@ import { handleServiceCommand } from './service-command.ts';
|
|
|
31
32
|
import { handleBundleCommand } from './bundle-command.ts';
|
|
32
33
|
import { buildListenerTestResult, formatListenerTestResult, handleSurfacesCommand } from './surface-command.ts';
|
|
33
34
|
import { buildControlPlaneStatusResult, formatControlPlaneStatus, handleSecrets, handleSessions, handleTasks, renderPairing, renderRemote, renderSubscriptions, renderWeb } from './management-commands.ts';
|
|
35
|
+
import { handleAgentKnowledgeCommand, handleCompatCommand, handleDelegateCommand } from './agent-knowledge-command.ts';
|
|
34
36
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
35
37
|
|
|
36
38
|
export interface CliCommandRuntime {
|
|
@@ -289,6 +291,14 @@ export function readAuthPaths(runtime: CliCommandRuntime) {
|
|
|
289
291
|
};
|
|
290
292
|
}
|
|
291
293
|
|
|
294
|
+
function createCliLocalUserAuthManager(runtime: CliCommandRuntime): UserAuthManager {
|
|
295
|
+
const paths = readAuthPaths(runtime);
|
|
296
|
+
return new UserAuthManager({
|
|
297
|
+
bootstrapFilePath: paths.userStorePath,
|
|
298
|
+
bootstrapCredentialPath: paths.bootstrapCredentialPath,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
292
302
|
export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promise<number> {
|
|
293
303
|
const prompt = runtime.cli.flags.prompt ?? runtime.cli.positionals.join(' ').trim();
|
|
294
304
|
if (!prompt) {
|
|
@@ -574,7 +584,7 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
574
584
|
}
|
|
575
585
|
|
|
576
586
|
async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
577
|
-
|
|
587
|
+
const localUserAuthManager = createCliLocalUserAuthManager(runtime);
|
|
578
588
|
const [sub = 'status', ...rawRest] = runtime.cli.commandArgs;
|
|
579
589
|
const rest = commandValues(rawRest);
|
|
580
590
|
if (sub === 'add-user' || sub === 'add') {
|
|
@@ -583,13 +593,13 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
|
583
593
|
const password = readPassword(rawRest);
|
|
584
594
|
if (!password) return 'Usage: goodvibes auth add-user <username> [--password <value>|--password-stdin] [--role <role>]';
|
|
585
595
|
const roles = readOptionValues(rawRest, '--role').filter((role) => role.length > 0);
|
|
586
|
-
const user =
|
|
596
|
+
const user = localUserAuthManager.addUser(username, password, roles.length > 0 ? roles : ['user']);
|
|
587
597
|
return `Auth user added: ${user.username} (${user.roles.join(', ') || 'no roles'})`;
|
|
588
598
|
}
|
|
589
599
|
if (sub === 'delete-user' || sub === 'remove-user') {
|
|
590
600
|
const username = rest[0];
|
|
591
601
|
if (!username) return 'Usage: goodvibes auth delete-user <username>';
|
|
592
|
-
return
|
|
602
|
+
return localUserAuthManager.deleteUser(username)
|
|
593
603
|
? `Auth user deleted: ${username}`
|
|
594
604
|
: `No auth user found: ${username}`;
|
|
595
605
|
}
|
|
@@ -598,31 +608,31 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
|
598
608
|
if (!username) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
|
|
599
609
|
const password = readPassword(rawRest);
|
|
600
610
|
if (!password) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
|
|
601
|
-
|
|
611
|
+
localUserAuthManager.rotatePassword(username, password);
|
|
602
612
|
return `Auth password rotated: ${username}`;
|
|
603
613
|
}
|
|
604
614
|
if (sub === 'revoke-session') {
|
|
605
615
|
const token = rest[0];
|
|
606
616
|
if (!token) return 'Usage: goodvibes auth revoke-session <token-or-fingerprint>';
|
|
607
|
-
return
|
|
617
|
+
return localUserAuthManager.revokeSession(token)
|
|
608
618
|
? 'Auth session revoked.'
|
|
609
619
|
: 'No auth session found.';
|
|
610
620
|
}
|
|
611
621
|
if (sub === 'revoke-sessions') {
|
|
612
622
|
const username = rest[0];
|
|
613
623
|
if (!username) return 'Usage: goodvibes auth revoke-sessions <username>';
|
|
614
|
-
const count =
|
|
624
|
+
const count = localUserAuthManager.revokeSessionsForUser(username);
|
|
615
625
|
return `Auth sessions revoked for ${username}: ${count}`;
|
|
616
626
|
}
|
|
617
627
|
if (sub === 'clear-bootstrap') {
|
|
618
|
-
return
|
|
628
|
+
return localUserAuthManager.clearBootstrapCredentialFile()
|
|
619
629
|
? 'Bootstrap credential file removed.'
|
|
620
630
|
: 'Bootstrap credential file was already absent.';
|
|
621
631
|
}
|
|
622
632
|
if (sub !== 'status' && sub !== 'list' && sub !== 'users' && sub !== 'sessions') {
|
|
623
633
|
return 'Usage: goodvibes auth [status|users|sessions|add-user|delete-user|rotate-password|revoke-session|revoke-sessions|clear-bootstrap]';
|
|
624
634
|
}
|
|
625
|
-
const snapshot =
|
|
635
|
+
const snapshot = localUserAuthManager.inspect();
|
|
626
636
|
const paths = readAuthPaths(runtime);
|
|
627
637
|
const value = {
|
|
628
638
|
...paths,
|
|
@@ -652,7 +662,6 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
|
652
662
|
` bootstrap credential: ${paths.bootstrapCredentialPresent ? 'present' : 'missing'} (${paths.bootstrapCredentialPath})`,
|
|
653
663
|
` operator tokens: ${paths.operatorTokenPresent ? 'present' : 'missing'} (${paths.operatorTokenPath})`,
|
|
654
664
|
].join('\n'));
|
|
655
|
-
});
|
|
656
665
|
}
|
|
657
666
|
|
|
658
667
|
|
|
@@ -684,6 +693,21 @@ export async function handleGoodVibesCliCommand(runtime: CliCommandRuntime): Pro
|
|
|
684
693
|
console.log(output);
|
|
685
694
|
return { handled: true, exitCode: exitCodeForText(output) };
|
|
686
695
|
}
|
|
696
|
+
case 'compat': {
|
|
697
|
+
const result = await handleCompatCommand(runtime);
|
|
698
|
+
console.log(result.output);
|
|
699
|
+
return { handled: true, exitCode: result.exitCode };
|
|
700
|
+
}
|
|
701
|
+
case 'knowledge': {
|
|
702
|
+
const result = await handleAgentKnowledgeCommand(runtime);
|
|
703
|
+
console.log(result.output);
|
|
704
|
+
return { handled: true, exitCode: result.exitCode };
|
|
705
|
+
}
|
|
706
|
+
case 'delegate': {
|
|
707
|
+
const result = await handleDelegateCommand(runtime);
|
|
708
|
+
console.log(result.output);
|
|
709
|
+
return { handled: true, exitCode: result.exitCode };
|
|
710
|
+
}
|
|
687
711
|
case 'subscription': {
|
|
688
712
|
const output = await renderSubscriptions(runtime);
|
|
689
713
|
console.log(output);
|
package/src/cli/parser.ts
CHANGED
|
@@ -26,6 +26,13 @@ const COMMAND_ALIASES: Readonly<Record<string, GoodVibesCliCommand>> = {
|
|
|
26
26
|
providers: 'providers',
|
|
27
27
|
provider: 'providers',
|
|
28
28
|
auth: 'auth',
|
|
29
|
+
compat: 'compat',
|
|
30
|
+
compatibility: 'compat',
|
|
31
|
+
knowledge: 'knowledge',
|
|
32
|
+
know: 'knowledge',
|
|
33
|
+
kb: 'knowledge',
|
|
34
|
+
delegate: 'delegate',
|
|
35
|
+
build: 'delegate',
|
|
29
36
|
subscription: 'subscription',
|
|
30
37
|
subscriptions: 'subscription',
|
|
31
38
|
secrets: 'secrets',
|
package/src/cli/types.ts
CHANGED
package/src/config/surface.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
2
2
|
import type { CommandContext } from './command-registry.ts';
|
|
3
|
+
import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
|
|
4
|
+
import { AgentSkillRegistry } from '../agent/skill-registry.ts';
|
|
3
5
|
|
|
4
6
|
export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
|
|
5
7
|
|
|
@@ -51,6 +53,10 @@ export interface AgentWorkspaceRuntimeSnapshot {
|
|
|
51
53
|
readonly daemonBaseUrl: string;
|
|
52
54
|
readonly daemonOwnership: 'external';
|
|
53
55
|
readonly sessionMemoryCount: number;
|
|
56
|
+
readonly localSkillCount: number;
|
|
57
|
+
readonly enabledSkillCount: number;
|
|
58
|
+
readonly localPersonaCount: number;
|
|
59
|
+
readonly activePersonaName: string;
|
|
54
60
|
readonly knowledgeRoute: '/api/goodvibes-agent/knowledge';
|
|
55
61
|
readonly knowledgeIsolation: 'agent-only';
|
|
56
62
|
readonly executionPolicy: 'serial-proactive';
|
|
@@ -98,6 +104,26 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
98
104
|
return 0;
|
|
99
105
|
}
|
|
100
106
|
})();
|
|
107
|
+
const personaSnapshot = (() => {
|
|
108
|
+
try {
|
|
109
|
+
const shellPaths = context.workspace?.shellPaths;
|
|
110
|
+
if (!shellPaths) return { count: 0, activeName: '(none)' };
|
|
111
|
+
const snapshot = AgentPersonaRegistry.fromShellPaths(shellPaths).snapshot();
|
|
112
|
+
return { count: snapshot.personas.length, activeName: snapshot.activePersona?.name ?? '(none)' };
|
|
113
|
+
} catch {
|
|
114
|
+
return { count: 0, activeName: '(unavailable)' };
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
const skillSnapshot = (() => {
|
|
118
|
+
try {
|
|
119
|
+
const shellPaths = context.workspace?.shellPaths;
|
|
120
|
+
if (!shellPaths) return { count: 0, enabled: 0 };
|
|
121
|
+
const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
|
|
122
|
+
return { count: snapshot.skills.length, enabled: snapshot.enabledSkills.length };
|
|
123
|
+
} catch {
|
|
124
|
+
return { count: 0, enabled: 0 };
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
101
127
|
const warnings: string[] = [];
|
|
102
128
|
if (provider === 'unknown' || model === 'unknown') warnings.push('Provider/model unavailable in this runtime context.');
|
|
103
129
|
if (!context.executeCommand) warnings.push('Command dispatch is unavailable; workspace actions will show guidance only.');
|
|
@@ -112,6 +138,10 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
112
138
|
daemonBaseUrl: `http://${host}:${port}`,
|
|
113
139
|
daemonOwnership: 'external',
|
|
114
140
|
sessionMemoryCount,
|
|
141
|
+
localSkillCount: skillSnapshot.count,
|
|
142
|
+
enabledSkillCount: skillSnapshot.enabled,
|
|
143
|
+
localPersonaCount: personaSnapshot.count,
|
|
144
|
+
activePersonaName: personaSnapshot.activeName,
|
|
115
145
|
knowledgeRoute: '/api/goodvibes-agent/knowledge',
|
|
116
146
|
knowledgeIsolation: 'agent-only',
|
|
117
147
|
executionPolicy: 'serial-proactive',
|
|
@@ -167,8 +197,8 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
167
197
|
detail: 'Memory, skills, and personas stay Agent-local until stable shared daemon registry contracts exist. Secrets must not be stored as memory.',
|
|
168
198
|
actions: [
|
|
169
199
|
{ id: 'memory', label: 'Open memory', detail: 'Inspect local/session memory commands and surfaces.', command: '/memory', kind: 'command', safety: 'read-only' },
|
|
170
|
-
{ id: 'skills', label: '
|
|
171
|
-
{ id: 'personas', label: 'Persona library', detail: 'Use local Agent personas to shape serial assistant behavior without spawning background agents.', kind: '
|
|
200
|
+
{ id: 'skills', label: 'Local skill library', detail: 'Create, review, and enable local Agent reusable procedures.', command: '/agent-skills', kind: 'command', safety: 'safe' },
|
|
201
|
+
{ id: 'personas', label: 'Persona library', detail: 'Use local Agent personas to shape serial assistant behavior without spawning background agents.', command: '/personas', kind: 'command', safety: 'safe' },
|
|
172
202
|
],
|
|
173
203
|
},
|
|
174
204
|
{
|
|
@@ -203,7 +233,7 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
203
233
|
detail: 'Agent does not become the coding TUI. Build, implement, fix, patch, and review work must be handed to GoodVibes TUI with the full original ask and WRFC only when explicitly requested.',
|
|
204
234
|
actions: [
|
|
205
235
|
{ id: 'delegate-guidance', label: 'Delegation rule', detail: 'For build/fix/review work, delegate one request to GoodVibes TUI instead of spawning local Engineer/Reviewer/Tester roots.', kind: 'guidance', safety: 'delegates' },
|
|
206
|
-
{ id: 'review-command', label: 'Review delegation command', detail: 'Use /
|
|
236
|
+
{ id: 'review-command', label: 'Review delegation command', detail: 'Use /delegate --wrfc only when the user explicitly asks for code review/build execution.', command: '/delegate --wrfc <task>', kind: 'command', safety: 'delegates' },
|
|
207
237
|
{ id: 'remote-policy', label: 'Remote runner policy', detail: 'Remote dispatch/rerun is blocked in Agent; TUI owns runner topology for delegated build work.', command: '/remote dispatch', kind: 'command', safety: 'blocked' },
|
|
208
238
|
],
|
|
209
239
|
},
|
|
@@ -180,7 +180,9 @@ export interface CommandExtensionRegistryServices {
|
|
|
180
180
|
|
|
181
181
|
export interface CommandExtensionServices
|
|
182
182
|
extends CommandExtensionRegistryServices,
|
|
183
|
-
CommandExtensionShellServices {
|
|
183
|
+
CommandExtensionShellServices {
|
|
184
|
+
readonly agentKnowledgeService?: import('@pellux/goodvibes-sdk/platform/knowledge').KnowledgeService;
|
|
185
|
+
}
|
|
184
186
|
|
|
185
187
|
/**
|
|
186
188
|
* CommandContext - Passed to every slash command handler so commands can
|
|
@@ -200,6 +202,7 @@ export interface CommandContext
|
|
|
200
202
|
readonly operator?: OperatorClient;
|
|
201
203
|
readonly peer?: PeerClient;
|
|
202
204
|
readonly providerApi?: ProviderApi;
|
|
205
|
+
readonly agentKnowledgeApi?: KnowledgeApi;
|
|
203
206
|
readonly knowledgeApi?: KnowledgeApi;
|
|
204
207
|
readonly hookApi?: HookApi;
|
|
205
208
|
readonly mcpApi?: McpApi;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { AgentSkillRegistry, type AgentSkillRecord } from '../../agent/skill-registry.ts';
|
|
2
|
+
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
3
|
+
import { requireShellPaths } from './runtime-services.ts';
|
|
4
|
+
|
|
5
|
+
interface ParsedSkillArgs {
|
|
6
|
+
readonly rest: readonly string[];
|
|
7
|
+
readonly flags: ReadonlyMap<string, string>;
|
|
8
|
+
readonly yes: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseSkillArgs(args: readonly string[]): ParsedSkillArgs {
|
|
12
|
+
const flags = new Map<string, string>();
|
|
13
|
+
const rest: string[] = [];
|
|
14
|
+
let yes = false;
|
|
15
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
16
|
+
const token = args[index] ?? '';
|
|
17
|
+
if (token === '--yes') {
|
|
18
|
+
yes = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (token.startsWith('--')) {
|
|
22
|
+
const key = token.slice(2);
|
|
23
|
+
const next = args[index + 1];
|
|
24
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
25
|
+
flags.set(key, next);
|
|
26
|
+
index += 1;
|
|
27
|
+
} else {
|
|
28
|
+
flags.set(key, 'true');
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
rest.push(token);
|
|
33
|
+
}
|
|
34
|
+
return { rest, flags, yes };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function splitList(value: string | undefined): readonly string[] {
|
|
38
|
+
if (!value) return [];
|
|
39
|
+
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function registryFromContext(ctx: CommandContext): AgentSkillRegistry {
|
|
43
|
+
return AgentSkillRegistry.fromShellPaths(requireShellPaths(ctx));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requiredFlag(flags: ReadonlyMap<string, string>, key: string): string {
|
|
47
|
+
const value = flags.get(key)?.trim();
|
|
48
|
+
if (!value) throw new Error(`Missing --${key}.`);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function summarizeSkill(skill: AgentSkillRecord): string {
|
|
53
|
+
const enabled = skill.enabled ? 'enabled' : 'disabled';
|
|
54
|
+
const tags = skill.tags.length > 0 ? ` tags=${skill.tags.join(',')}` : '';
|
|
55
|
+
return ` ${skill.id} ${enabled} ${skill.reviewState} ${skill.name} - ${skill.description}${tags}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderList(title: string, registry: AgentSkillRegistry, skills: readonly AgentSkillRecord[]): string {
|
|
59
|
+
const snapshot = registry.snapshot();
|
|
60
|
+
if (skills.length === 0) {
|
|
61
|
+
return `${title}\n No local Agent skills yet. Create one with /agent-skills create --name <name> --description <summary> --procedure <steps>.`;
|
|
62
|
+
}
|
|
63
|
+
return [
|
|
64
|
+
`${title} (${skills.length})`,
|
|
65
|
+
` store: ${snapshot.path}`,
|
|
66
|
+
` enabled: ${snapshot.enabledSkills.length}`,
|
|
67
|
+
...skills.map(summarizeSkill),
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderSkill(skill: AgentSkillRecord): string {
|
|
72
|
+
return [
|
|
73
|
+
`Skill ${skill.name}`,
|
|
74
|
+
` id: ${skill.id}`,
|
|
75
|
+
` enabled: ${skill.enabled ? 'yes' : 'no'}`,
|
|
76
|
+
` review: ${skill.reviewState}`,
|
|
77
|
+
` source: ${skill.source}`,
|
|
78
|
+
` provenance: ${skill.provenance}`,
|
|
79
|
+
` tags: ${skill.tags.join(', ') || '(none)'}`,
|
|
80
|
+
` triggers: ${skill.triggers.join(', ') || '(manual)'}`,
|
|
81
|
+
` created: ${skill.createdAt}`,
|
|
82
|
+
` updated: ${skill.updatedAt}`,
|
|
83
|
+
skill.staleReason ? ` stale reason: ${skill.staleReason}` : '',
|
|
84
|
+
'',
|
|
85
|
+
skill.description,
|
|
86
|
+
'',
|
|
87
|
+
skill.procedure,
|
|
88
|
+
].filter(Boolean).join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printError(ctx: CommandContext, error: unknown): void {
|
|
92
|
+
ctx.print(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
|
|
96
|
+
const sub = (args[0] ?? 'list').toLowerCase();
|
|
97
|
+
const skillRegistry = registryFromContext(ctx);
|
|
98
|
+
try {
|
|
99
|
+
if (sub === 'list' || sub === 'open') {
|
|
100
|
+
ctx.print(renderList('Agent Skills', skillRegistry, skillRegistry.list()));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (sub === 'enabled') {
|
|
104
|
+
const snapshot = skillRegistry.snapshot();
|
|
105
|
+
ctx.print(renderList('Enabled Agent Skills', skillRegistry, snapshot.enabledSkills));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (sub === 'search') {
|
|
109
|
+
const query = args.slice(1).join(' ').trim();
|
|
110
|
+
ctx.print(renderList(query ? `Agent Skills matching "${query}"` : 'Agent Skills', skillRegistry, skillRegistry.search(query)));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (sub === 'show') {
|
|
114
|
+
const id = args[1];
|
|
115
|
+
if (!id) {
|
|
116
|
+
ctx.print('Usage: /agent-skills show <id>');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const skill = skillRegistry.get(id);
|
|
120
|
+
ctx.print(skill ? renderSkill(skill) : `Unknown Agent skill: ${id}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (sub === 'create') {
|
|
124
|
+
const parsed = parseSkillArgs(args.slice(1));
|
|
125
|
+
const procedure = parsed.flags.get('procedure')?.trim() || parsed.rest.join(' ').trim();
|
|
126
|
+
const skill = skillRegistry.create({
|
|
127
|
+
name: requiredFlag(parsed.flags, 'name'),
|
|
128
|
+
description: requiredFlag(parsed.flags, 'description'),
|
|
129
|
+
procedure,
|
|
130
|
+
triggers: splitList(parsed.flags.get('triggers')),
|
|
131
|
+
tags: splitList(parsed.flags.get('tags')),
|
|
132
|
+
enabled: parsed.flags.get('enabled') === 'true',
|
|
133
|
+
source: 'user',
|
|
134
|
+
provenance: 'slash-command',
|
|
135
|
+
});
|
|
136
|
+
ctx.print(`Created Agent skill ${skill.id}: ${skill.name}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (sub === 'update') {
|
|
140
|
+
const id = args[1];
|
|
141
|
+
if (!id) {
|
|
142
|
+
ctx.print('Usage: /agent-skills update <id> [--name ...] [--description ...] [--procedure ...]');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const parsed = parseSkillArgs(args.slice(2));
|
|
146
|
+
const updated = skillRegistry.update(id, {
|
|
147
|
+
name: parsed.flags.get('name'),
|
|
148
|
+
description: parsed.flags.get('description'),
|
|
149
|
+
procedure: parsed.flags.get('procedure'),
|
|
150
|
+
triggers: parsed.flags.has('triggers') ? splitList(parsed.flags.get('triggers')) : undefined,
|
|
151
|
+
tags: parsed.flags.has('tags') ? splitList(parsed.flags.get('tags')) : undefined,
|
|
152
|
+
provenance: 'slash-command',
|
|
153
|
+
});
|
|
154
|
+
ctx.print(`Updated Agent skill ${updated.id}: ${updated.name}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (sub === 'enable' || sub === 'disable') {
|
|
158
|
+
const id = args[1];
|
|
159
|
+
if (!id) {
|
|
160
|
+
ctx.print(`Usage: /agent-skills ${sub} <id>`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const skill = skillRegistry.setEnabled(id, sub === 'enable');
|
|
164
|
+
ctx.print(`${sub === 'enable' ? 'Enabled' : 'Disabled'} Agent skill ${skill.id}: ${skill.name}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (sub === 'review') {
|
|
168
|
+
const id = args[1];
|
|
169
|
+
if (!id) {
|
|
170
|
+
ctx.print('Usage: /agent-skills review <id>');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const skill = skillRegistry.markReviewed(id);
|
|
174
|
+
ctx.print(`Reviewed Agent skill ${skill.id}.`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (sub === 'stale') {
|
|
178
|
+
const id = args[1];
|
|
179
|
+
if (!id) {
|
|
180
|
+
ctx.print('Usage: /agent-skills stale <id> <reason...>');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const skill = skillRegistry.markStale(id, args.slice(2).join(' '));
|
|
184
|
+
ctx.print(`Marked Agent skill ${skill.id} stale.`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (sub === 'delete' || sub === 'remove') {
|
|
188
|
+
const parsed = parseSkillArgs(args.slice(1));
|
|
189
|
+
const id = parsed.rest[0];
|
|
190
|
+
if (!id) {
|
|
191
|
+
ctx.print('Usage: /agent-skills delete <id> --yes');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!parsed.yes) {
|
|
195
|
+
ctx.print(`Refusing to delete Agent skill ${id} without --yes.`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const removed = skillRegistry.deleteSkill(id);
|
|
199
|
+
ctx.print(`Deleted Agent skill ${removed.id}: ${removed.name}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
ctx.print('Usage: /agent-skills [list|enabled|search|show|create|update|enable|disable|review|stale|delete]');
|
|
203
|
+
} catch (error) {
|
|
204
|
+
printError(ctx, error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function registerAgentSkillsRuntimeCommands(registry: CommandRegistry): void {
|
|
209
|
+
registry.register({
|
|
210
|
+
name: 'agent-skills',
|
|
211
|
+
aliases: ['askills', 'local-skills'],
|
|
212
|
+
description: 'Manage local GoodVibes Agent skills',
|
|
213
|
+
usage: '[list|enabled|search <query>|show <id>|create --name <name> --description <summary> --procedure <steps>|update <id> [--name ...] [--description ...] [--procedure ...]|enable <id>|disable <id>|review <id>|stale <id> <reason...>|delete <id> --yes]',
|
|
214
|
+
handler: runAgentSkillsRuntimeCommand,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
2
|
+
import type { SharedSessionParticipant } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
3
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
|
+
|
|
5
|
+
function hasFlag(args: readonly string[], flag: string): boolean {
|
|
6
|
+
return args.includes(flag);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function delegationTaskValues(args: readonly string[]): string[] {
|
|
10
|
+
const values: string[] = [];
|
|
11
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
12
|
+
const token = args[index]!;
|
|
13
|
+
if (token === '--wrfc') continue;
|
|
14
|
+
if (!token.startsWith('--')) {
|
|
15
|
+
values.push(token);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return values;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildDelegationBody(task: string, wrfcRequested: boolean): string {
|
|
23
|
+
return [
|
|
24
|
+
'GoodVibes Agent explicit build delegation.',
|
|
25
|
+
'',
|
|
26
|
+
'Original user ask:',
|
|
27
|
+
task,
|
|
28
|
+
'',
|
|
29
|
+
'Agent policy:',
|
|
30
|
+
'- GoodVibes Agent is not the coding TUI.',
|
|
31
|
+
'- Preserve the full original ask.',
|
|
32
|
+
'- GoodVibes TUI owns file edits, git/worktree flows, sandbox/QEMU UX, and any WRFC owner chain.',
|
|
33
|
+
wrfcRequested
|
|
34
|
+
? '- WRFC was explicitly requested by the Agent user for this build/fix/review delegation.'
|
|
35
|
+
: '- WRFC was not explicitly requested; do not turn this into WRFC solely because it came from Agent.',
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerDelegationRuntimeCommands(registry: CommandRegistry): void {
|
|
40
|
+
const makeHandler = (defaultWrfc: boolean) => async (args: string[], ctx: CommandContext): Promise<void> => {
|
|
41
|
+
const wrfcRequested = defaultWrfc || hasFlag(args, '--wrfc');
|
|
42
|
+
const task = delegationTaskValues(args).join(' ').trim();
|
|
43
|
+
if (!task) {
|
|
44
|
+
ctx.print(defaultWrfc ? 'Usage: /wrfc <build/fix/review task>' : 'Usage: /delegate [--wrfc] <build/fix/review task>');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const operator = ctx.clients?.operator;
|
|
48
|
+
if (!operator) {
|
|
49
|
+
ctx.print([
|
|
50
|
+
'Delegation unavailable: no operator client is attached.',
|
|
51
|
+
'Use the external daemon/shared-session route from a configured Agent runtime, or open GoodVibes TUI in the target workspace.',
|
|
52
|
+
].join('\n'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const participant = {
|
|
57
|
+
surfaceKind: 'service',
|
|
58
|
+
surfaceId: 'goodvibes-agent',
|
|
59
|
+
externalId: ctx.session.runtime.sessionId,
|
|
60
|
+
displayName: 'GoodVibes Agent',
|
|
61
|
+
lastSeenAt: Date.now(),
|
|
62
|
+
} satisfies SharedSessionParticipant;
|
|
63
|
+
const session = await operator.sessions.ensureSession({
|
|
64
|
+
title: `Agent delegation: ${task.slice(0, 72)}`,
|
|
65
|
+
participant,
|
|
66
|
+
metadata: {
|
|
67
|
+
originSurface: 'goodvibes-agent',
|
|
68
|
+
sourceSessionId: ctx.session.runtime.sessionId,
|
|
69
|
+
task,
|
|
70
|
+
wrfcRequested,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
await operator.sessions.submitMessage({
|
|
74
|
+
sessionId: session.id,
|
|
75
|
+
body: buildDelegationBody(task, wrfcRequested),
|
|
76
|
+
surfaceKind: participant.surfaceKind,
|
|
77
|
+
surfaceId: participant.surfaceId,
|
|
78
|
+
externalId: participant.externalId,
|
|
79
|
+
displayName: participant.displayName,
|
|
80
|
+
title: `Agent delegation: ${task.slice(0, 72)}`,
|
|
81
|
+
metadata: {
|
|
82
|
+
originSurface: 'goodvibes-agent',
|
|
83
|
+
sourceSessionId: ctx.session.runtime.sessionId,
|
|
84
|
+
kind: 'task',
|
|
85
|
+
task,
|
|
86
|
+
wrfcRequested,
|
|
87
|
+
},
|
|
88
|
+
routing: {
|
|
89
|
+
executionIntent: {
|
|
90
|
+
riskClass: 'elevated',
|
|
91
|
+
requiresApproval: true,
|
|
92
|
+
networkPolicy: 'inherit',
|
|
93
|
+
filesystemPolicy: 'workspace-write',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
ctx.print([
|
|
98
|
+
'Delegation submitted to GoodVibes TUI/shared-session routes.',
|
|
99
|
+
` session: ${session.id}`,
|
|
100
|
+
` mode: ${wrfcRequested ? 'WRFC requested' : 'direct build delegation'}`,
|
|
101
|
+
` task: ${task}`,
|
|
102
|
+
' next: check GoodVibes TUI shared-session/task status for the result.',
|
|
103
|
+
].join('\n'));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
ctx.print([
|
|
106
|
+
'Delegation failed.',
|
|
107
|
+
` error: ${summarizeError(error)}`,
|
|
108
|
+
' fallback: open GoodVibes TUI in the target workspace and paste the original task there.',
|
|
109
|
+
].join('\n'));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
registry.register({
|
|
114
|
+
name: 'delegate',
|
|
115
|
+
aliases: ['build'],
|
|
116
|
+
description: 'Explicitly delegate build/fix/review work to GoodVibes TUI through shared-session routes',
|
|
117
|
+
usage: '[--wrfc] <task>',
|
|
118
|
+
argsHint: '[--wrfc] <task>',
|
|
119
|
+
handler: makeHandler(false),
|
|
120
|
+
});
|
|
121
|
+
registry.register({
|
|
122
|
+
name: 'wrfc',
|
|
123
|
+
aliases: ['review'],
|
|
124
|
+
description: 'Explicitly delegate build/fix/review work to GoodVibes TUI with WRFC requested',
|
|
125
|
+
usage: '<task>',
|
|
126
|
+
argsHint: '<task>',
|
|
127
|
+
handler: makeHandler(true),
|
|
128
|
+
});
|
|
129
|
+
}
|