@pellux/goodvibes-agent 0.1.80 → 0.1.82
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 +11 -0
- package/package.json +3 -1
- package/src/agent/skill-registry.ts +255 -5
- package/src/cli/help.ts +4 -4
- package/src/cli/management.ts +54 -136
- package/src/cli/status.ts +10 -10
- package/src/input/agent-workspace-categories.ts +2 -0
- package/src/input/agent-workspace-setup.ts +6 -4
- package/src/input/agent-workspace-snapshot.ts +26 -3
- package/src/input/agent-workspace-types.ts +4 -0
- package/src/input/commands/agent-skills-runtime.ts +151 -3
- package/src/input/commands/health-runtime.ts +12 -14
- package/src/input/commands/platform-access-runtime.ts +8 -4
- package/src/input/commands.ts +0 -2
- package/src/input/onboarding/onboarding-wizard-helpers.ts +1 -1
- package/src/panels/builtin/operations.ts +0 -10
- package/src/panels/provider-health-domains.ts +9 -8
- package/src/renderer/agent-workspace.ts +9 -6
- package/src/runtime/onboarding/apply.ts +6 -154
- package/src/runtime/onboarding/derivation.ts +4 -4
- package/src/runtime/onboarding/types.ts +1 -11
- package/src/runtime/onboarding/verify.ts +3 -25
- package/src/version.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +0 -128
- package/src/panels/local-auth-panel.ts +0 -130
package/src/cli/management.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
@@ -21,7 +21,6 @@ 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';
|
|
25
24
|
import type { GoodVibesCliParseResult } from './types.ts';
|
|
26
25
|
import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
|
|
27
26
|
import { classifyProviderSetup } from './provider-classification.ts';
|
|
@@ -60,7 +59,7 @@ export function formatJsonOrText(cli: GoodVibesCliParseResult): Formatter {
|
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
function exitCodeForText(output: string): number {
|
|
63
|
-
if (output.startsWith('Usage:') || output.startsWith('Invalid ')) return 2;
|
|
62
|
+
if (output.startsWith('Usage:') || output.startsWith('Invalid ') || output.startsWith('Unsupported:')) return 2;
|
|
64
63
|
if (output.startsWith('Session not found:') || output.startsWith('Unknown task:') || output.startsWith('Task submit failed ')) return 1;
|
|
65
64
|
if (output.startsWith('No stored ') || output.startsWith('No pending ') || output.startsWith('No model ') || output.startsWith('No provider ') || output.startsWith('No auth ')) return 1;
|
|
66
65
|
if (output.startsWith('Unknown ')) return 1;
|
|
@@ -74,59 +73,10 @@ function splitCommandOption(token: string): { readonly name: string; readonly va
|
|
|
74
73
|
return { name: token.slice(0, index), value: token.slice(index + 1) };
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
function readOptionValue(args: readonly string[], name: string): string | undefined {
|
|
78
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
79
|
-
const token = args[index]!;
|
|
80
|
-
const split = splitCommandOption(token);
|
|
81
|
-
if (split.name !== name) continue;
|
|
82
|
-
if (split.value !== undefined) return split.value;
|
|
83
|
-
const next = args[index + 1];
|
|
84
|
-
if (next === undefined || next.startsWith('--')) return undefined;
|
|
85
|
-
return next;
|
|
86
|
-
}
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function readOptionValues(args: readonly string[], name: string): string[] {
|
|
91
|
-
const values: string[] = [];
|
|
92
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
93
|
-
const token = args[index]!;
|
|
94
|
-
const split = splitCommandOption(token);
|
|
95
|
-
if (split.name !== name) continue;
|
|
96
|
-
if (split.value !== undefined) {
|
|
97
|
-
values.push(split.value);
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const next = args[index + 1];
|
|
101
|
-
if (next !== undefined && !next.startsWith('--')) values.push(next);
|
|
102
|
-
}
|
|
103
|
-
return values;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
76
|
export function hasCommandFlag(args: readonly string[], name: string): boolean {
|
|
107
77
|
return args.some((arg) => splitCommandOption(arg).name === name);
|
|
108
78
|
}
|
|
109
79
|
|
|
110
|
-
function commandValues(args: readonly string[]): string[] {
|
|
111
|
-
const values: string[] = [];
|
|
112
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
113
|
-
const token = args[index]!;
|
|
114
|
-
if (!token.startsWith('--')) {
|
|
115
|
-
values.push(token);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (!token.includes('=') && args[index + 1] && !args[index + 1]!.startsWith('--')) index += 1;
|
|
119
|
-
}
|
|
120
|
-
return values;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function readPassword(args: readonly string[]): string | null {
|
|
124
|
-
const explicit = readOptionValue(args, '--password');
|
|
125
|
-
if (explicit !== undefined) return explicit;
|
|
126
|
-
if (hasCommandFlag(args, '--password-stdin')) return readFileSync(0, 'utf-8').trimEnd();
|
|
127
|
-
return process.env.GOODVIBES_AUTH_PASSWORD ?? null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
80
|
export function extractAuthorizationCode(input: string): string {
|
|
131
81
|
try {
|
|
132
82
|
const url = new URL(input);
|
|
@@ -291,14 +241,6 @@ export function readAuthPaths(runtime: CliCommandRuntime) {
|
|
|
291
241
|
};
|
|
292
242
|
}
|
|
293
243
|
|
|
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
|
-
|
|
302
244
|
export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promise<number> {
|
|
303
245
|
const prompt = runtime.cli.flags.prompt ?? runtime.cli.positionals.join(' ').trim();
|
|
304
246
|
if (!prompt) {
|
|
@@ -584,84 +526,60 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
584
526
|
}
|
|
585
527
|
|
|
586
528
|
async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (sub === 'revoke-sessions') {
|
|
622
|
-
const username = rest[0];
|
|
623
|
-
if (!username) return 'Usage: goodvibes auth revoke-sessions <username>';
|
|
624
|
-
const count = localUserAuthManager.revokeSessionsForUser(username);
|
|
625
|
-
return `Auth sessions revoked for ${username}: ${count}`;
|
|
626
|
-
}
|
|
627
|
-
if (sub === 'clear-bootstrap') {
|
|
628
|
-
return localUserAuthManager.clearBootstrapCredentialFile()
|
|
629
|
-
? 'Bootstrap credential file removed.'
|
|
630
|
-
: 'Bootstrap credential file was already absent.';
|
|
631
|
-
}
|
|
632
|
-
if (sub !== 'status' && sub !== 'list' && sub !== 'users' && sub !== 'sessions') {
|
|
633
|
-
return 'Usage: goodvibes auth [status|users|sessions|add-user|delete-user|rotate-password|revoke-session|revoke-sessions|clear-bootstrap]';
|
|
634
|
-
}
|
|
635
|
-
const snapshot = localUserAuthManager.inspect();
|
|
636
|
-
const paths = readAuthPaths(runtime);
|
|
637
|
-
const value = {
|
|
638
|
-
...paths,
|
|
639
|
-
users: snapshot.users.map((user) => ({ username: user.username, roles: user.roles })),
|
|
640
|
-
sessions: snapshot.sessions.length,
|
|
641
|
-
permissionMode: runtime.configManager.get('permissions.mode'),
|
|
642
|
-
};
|
|
643
|
-
if (sub === 'users') {
|
|
644
|
-
return formatJsonOrText(runtime.cli)(value.users, [
|
|
645
|
-
`GoodVibes auth users (${value.users.length})`,
|
|
646
|
-
...value.users.map((user) => ` ${user.username} (${user.roles.join(', ') || 'no roles'})`),
|
|
647
|
-
].join('\n'));
|
|
648
|
-
}
|
|
649
|
-
if (sub === 'sessions') {
|
|
650
|
-
return formatJsonOrText(runtime.cli)(snapshot.sessions, [
|
|
651
|
-
`GoodVibes auth sessions (${snapshot.sessions.length})`,
|
|
652
|
-
...snapshot.sessions.map((session) => ` ${session.username} expires=${new Date(session.expiresAt).toISOString()} fingerprint=${session.tokenFingerprint}`),
|
|
653
|
-
].join('\n'));
|
|
654
|
-
}
|
|
529
|
+
const [sub = 'status'] = runtime.cli.commandArgs;
|
|
530
|
+
const blocked = new Set([
|
|
531
|
+
'add-user',
|
|
532
|
+
'add',
|
|
533
|
+
'delete-user',
|
|
534
|
+
'remove-user',
|
|
535
|
+
'rotate-password',
|
|
536
|
+
'passwd',
|
|
537
|
+
'revoke-session',
|
|
538
|
+
'revoke-sessions',
|
|
539
|
+
'clear-bootstrap',
|
|
540
|
+
]);
|
|
541
|
+
if (blocked.has(sub)) {
|
|
542
|
+
return [
|
|
543
|
+
'Unsupported: runtime auth user/session administration is external to GoodVibes Agent.',
|
|
544
|
+
'GoodVibes Agent connects to an already-running runtime and does not create, delete, rotate, revoke, or clear runtime users, sessions, or bootstrap credentials.',
|
|
545
|
+
'Use the runtime-owning GoodVibes TUI or host tooling for runtime auth administration.',
|
|
546
|
+
].join('\n');
|
|
547
|
+
}
|
|
548
|
+
if (sub !== 'status' && sub !== 'review' && sub !== 'list' && sub !== 'users' && sub !== 'sessions') {
|
|
549
|
+
return 'Usage: goodvibes-agent auth [status|review|users|sessions]';
|
|
550
|
+
}
|
|
551
|
+
const paths = readAuthPaths(runtime);
|
|
552
|
+
const value = {
|
|
553
|
+
authOwner: 'external-runtime',
|
|
554
|
+
operatorTokenPresent: paths.operatorTokenPresent,
|
|
555
|
+
operatorTokenPath: paths.operatorTokenPath,
|
|
556
|
+
compatibilityUserStorePresent: paths.userStorePresent,
|
|
557
|
+
compatibilityUserStorePath: paths.userStorePath,
|
|
558
|
+
compatibilityBootstrapCredentialPresent: paths.bootstrapCredentialPresent,
|
|
559
|
+
compatibilityBootstrapCredentialPath: paths.bootstrapCredentialPath,
|
|
560
|
+
permissionMode: runtime.configManager.get('permissions.mode'),
|
|
561
|
+
};
|
|
562
|
+
if (sub === 'users' || sub === 'sessions') {
|
|
655
563
|
return formatJsonOrText(runtime.cli)(value, [
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
`
|
|
659
|
-
|
|
660
|
-
`
|
|
661
|
-
|
|
662
|
-
` bootstrap credential: ${paths.bootstrapCredentialPresent ? 'present' : 'missing'} (${paths.bootstrapCredentialPath})`,
|
|
663
|
-
` operator tokens: ${paths.operatorTokenPresent ? 'present' : 'missing'} (${paths.operatorTokenPath})`,
|
|
564
|
+
`GoodVibes Agent auth ${sub}`,
|
|
565
|
+
' owner: external GoodVibes runtime',
|
|
566
|
+
` operator token: ${paths.operatorTokenPresent ? 'present' : 'missing'}`,
|
|
567
|
+
` operator token path: ${paths.operatorTokenPath}`,
|
|
568
|
+
` ${sub}: managed by the runtime-owning TUI or host tooling`,
|
|
569
|
+
' Agent does not enumerate or mutate runtime users/sessions from the local CLI.',
|
|
664
570
|
].join('\n'));
|
|
571
|
+
}
|
|
572
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
573
|
+
'GoodVibes Agent auth',
|
|
574
|
+
' owner: external GoodVibes runtime',
|
|
575
|
+
` permission mode: ${String(value.permissionMode)}`,
|
|
576
|
+
` operator token: ${paths.operatorTokenPresent ? 'present' : 'missing'} (${paths.operatorTokenPath})`,
|
|
577
|
+
` compatibility user store: ${paths.userStorePresent ? 'present' : 'missing'} (${paths.userStorePath})`,
|
|
578
|
+
` compatibility bootstrap credential: ${paths.bootstrapCredentialPresent ? 'present' : 'missing'} (${paths.bootstrapCredentialPath})`,
|
|
579
|
+
' runtime user/session administration: external',
|
|
580
|
+
' next: goodvibes-agent providers',
|
|
581
|
+
' next: goodvibes-agent subscription providers',
|
|
582
|
+
].join('\n'));
|
|
665
583
|
}
|
|
666
584
|
|
|
667
585
|
|
package/src/cli/status.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface CliStatusSnapshot {
|
|
|
50
50
|
readonly permissionLabel: string;
|
|
51
51
|
readonly secretPolicy: unknown;
|
|
52
52
|
readonly secretPolicyLabel: string;
|
|
53
|
-
readonly
|
|
53
|
+
readonly runtimeAuthSignal: CliAuthStatus | null;
|
|
54
54
|
};
|
|
55
55
|
readonly runtimeConnection: {
|
|
56
56
|
readonly enabled: unknown;
|
|
@@ -182,13 +182,13 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
182
182
|
|
|
183
183
|
if (networkFacingSurfaces.length > 0 && options.auth?.userStorePresent !== true) {
|
|
184
184
|
findings.push({
|
|
185
|
-
id: 'network-endpoint-without-
|
|
185
|
+
id: 'network-endpoint-without-runtime-auth-signal',
|
|
186
186
|
area: 'auth',
|
|
187
187
|
severity: 'risk',
|
|
188
|
-
summary: 'Network-facing runtime endpoints are enabled
|
|
189
|
-
cause: `${networkFacingSurfaces.map(([name]) => name).join(', ')} are LAN/custom-bound, but
|
|
190
|
-
impact: 'Remote access paths may be unusable or unsafe
|
|
191
|
-
action: '
|
|
188
|
+
summary: 'Network-facing runtime endpoints are enabled without a visible runtime auth signal.',
|
|
189
|
+
cause: `${networkFacingSurfaces.map(([name]) => name).join(', ')} are LAN/custom-bound, but Agent cannot see runtime auth state from its local compatibility files.`,
|
|
190
|
+
impact: 'Remote access paths may be unusable or unsafe unless the external runtime owner configured auth.',
|
|
191
|
+
action: 'Review runtime auth from the owning GoodVibes TUI or host tooling; Agent will not create local runtime users.',
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -200,7 +200,7 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
200
200
|
summary: 'A bootstrap credential is still present while network-facing surfaces are enabled.',
|
|
201
201
|
cause: `${networkFacingSurfaces.map(([name]) => name).join(', ')} are LAN/custom-bound and auth-bootstrap.txt exists.`,
|
|
202
202
|
impact: 'Bootstrap credentials should be treated as temporary setup material, not long-lived network access credentials.',
|
|
203
|
-
action: '
|
|
203
|
+
action: 'Use the runtime-owning GoodVibes TUI or host tooling to replace bootstrap auth and retire the bootstrap credential.',
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -264,7 +264,7 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
|
|
|
264
264
|
permissionLabel: permissionModeLabel(config.get('permissions.mode')),
|
|
265
265
|
secretPolicy: config.get('storage.secretPolicy'),
|
|
266
266
|
secretPolicyLabel: secretPolicyLabel(config.get('storage.secretPolicy')),
|
|
267
|
-
|
|
267
|
+
runtimeAuthSignal: options.auth ?? null,
|
|
268
268
|
},
|
|
269
269
|
runtimeConnection: {
|
|
270
270
|
enabled: config.get('service.enabled'),
|
|
@@ -317,8 +317,8 @@ export function renderCliStatus(options: CliStatusOptions): string {
|
|
|
317
317
|
` permissions: ${permissionModeLabel(config.get('permissions.mode'))} (${String(config.get('permissions.mode'))})`,
|
|
318
318
|
` secretPolicy: ${secretPolicyLabel(config.get('storage.secretPolicy'))} (${String(config.get('storage.secretPolicy'))})`,
|
|
319
319
|
options.auth
|
|
320
|
-
? `
|
|
321
|
-
: '
|
|
320
|
+
? ` runtimeAuthSignal: ${options.auth.userStorePresent ? 'present' : 'missing'} (${options.auth.userStorePath})`
|
|
321
|
+
: ' runtimeAuthSignal: unknown',
|
|
322
322
|
options.auth
|
|
323
323
|
? ` bootstrapCredential: ${options.auth.bootstrapCredentialPresent ? 'present' : 'missing'} (${options.auth.bootstrapCredentialPath})`
|
|
324
324
|
: ' bootstrapCredential: unknown',
|
|
@@ -144,6 +144,8 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
144
144
|
actions: [
|
|
145
145
|
{ id: 'skills-list', label: 'List skills', detail: 'Print the full local Agent skill library.', command: '/agent-skills list', kind: 'command', safety: 'read-only' },
|
|
146
146
|
{ id: 'skills-enabled', label: 'Enabled skills', detail: 'Show only skills currently injected into Agent guidance.', command: '/agent-skills enabled', kind: 'command', safety: 'read-only' },
|
|
147
|
+
{ id: 'skills-bundles', label: 'Skill bundles', detail: 'List reviewable groups of local skills that can be enabled together.', command: '/agent-skills bundle list', kind: 'command', safety: 'read-only' },
|
|
148
|
+
{ id: 'skills-create-bundle', label: 'Create bundle', detail: 'Create a named skill bundle from existing skill ids with an explicit command.', command: '/agent-skills bundle create --name <name> --description <summary> --skills <id,id>', kind: 'command', safety: 'safe' },
|
|
147
149
|
{ id: 'skills-prev', label: 'Previous skill', detail: 'Move the local skill selection up without changing enabled state.', localKind: 'skill', selectionDelta: -1, kind: 'local-selection', safety: 'safe' },
|
|
148
150
|
{ id: 'skills-next', label: 'Next skill', detail: 'Move the local skill selection down without changing enabled state.', localKind: 'skill', selectionDelta: 1, kind: 'local-selection', safety: 'safe' },
|
|
149
151
|
{ id: 'skills-create', label: 'Create skill', detail: 'Open an in-workspace form for a reusable local procedure. No placeholder command is dispatched.', editorKind: 'skill', kind: 'editor', safety: 'safe' },
|
|
@@ -17,6 +17,8 @@ export interface AgentWorkspaceSetupChecklistInput {
|
|
|
17
17
|
readonly enabledRoutineCount: number;
|
|
18
18
|
readonly skillCount: number;
|
|
19
19
|
readonly enabledSkillCount: number;
|
|
20
|
+
readonly skillBundleCount: number;
|
|
21
|
+
readonly enabledSkillBundleCount: number;
|
|
20
22
|
readonly activePersonaName: string;
|
|
21
23
|
readonly readyChannelCount: number;
|
|
22
24
|
readonly voiceProviderCount: number;
|
|
@@ -77,10 +79,10 @@ export function buildAgentWorkspaceSetupChecklist(input: AgentWorkspaceSetupChec
|
|
|
77
79
|
{
|
|
78
80
|
id: 'skills',
|
|
79
81
|
label: 'Skills',
|
|
80
|
-
status:
|
|
81
|
-
detail: input.skillCount > 0
|
|
82
|
-
? `${input.enabledSkillCount}/${input.skillCount} local skill(s) enabled.`
|
|
83
|
-
: 'Create reusable local skills for repeated workflows.',
|
|
82
|
+
status: input.enabledSkillCount > 0 || input.enabledSkillBundleCount > 0 ? 'ready' : input.skillCount > 0 || input.skillBundleCount > 0 ? 'recommended' : 'optional',
|
|
83
|
+
detail: input.skillCount > 0 || input.skillBundleCount > 0
|
|
84
|
+
? `${input.enabledSkillCount}/${input.skillCount} local skill(s) enabled; ${input.enabledSkillBundleCount}/${input.skillBundleCount} bundle(s) enabled.`
|
|
85
|
+
: 'Create reusable local skills and bundles for repeated workflows.',
|
|
84
86
|
command: '/agent-skills',
|
|
85
87
|
},
|
|
86
88
|
{
|
|
@@ -2,7 +2,7 @@ import { basename, sep } from 'node:path';
|
|
|
2
2
|
import type { CommandContext } from './command-registry.ts';
|
|
3
3
|
import { AgentPersonaRegistry, type AgentPersonaRecord } from '../agent/persona-registry.ts';
|
|
4
4
|
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
|
|
5
|
-
import { AgentSkillRegistry, type AgentSkillRecord } from '../agent/skill-registry.ts';
|
|
5
|
+
import { AgentSkillRegistry, type AgentSkillBundleRecord, type AgentSkillRecord } from '../agent/skill-registry.ts';
|
|
6
6
|
import { getAgentRuntimeProfilesRoot, listAgentRuntimeProfiles, listAgentRuntimeProfileTemplates } from '../agent/runtime-profile.ts';
|
|
7
7
|
import { buildAgentWorkspaceChannels } from './agent-workspace-channels.ts';
|
|
8
8
|
import { buildAgentWorkspaceSetupChecklist } from './agent-workspace-setup.ts';
|
|
@@ -86,6 +86,19 @@ function summarizeSkillItem(skill: AgentSkillRecord): AgentWorkspaceLocalLibrary
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function summarizeSkillBundleItem(bundle: AgentSkillBundleRecord): AgentWorkspaceLocalLibraryItem {
|
|
90
|
+
return {
|
|
91
|
+
id: bundle.id,
|
|
92
|
+
name: bundle.name,
|
|
93
|
+
description: `${bundle.description} Skills: ${bundle.skillIds.join(', ')}`,
|
|
94
|
+
reviewState: bundle.reviewState,
|
|
95
|
+
source: bundle.source,
|
|
96
|
+
tags: bundle.skillIds,
|
|
97
|
+
triggers: [],
|
|
98
|
+
enabled: bundle.enabled,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
89
102
|
function summarizeRoutineItem(routine: AgentRoutineRecord): AgentWorkspaceLocalLibraryItem {
|
|
90
103
|
return {
|
|
91
104
|
id: routine.id,
|
|
@@ -158,15 +171,19 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
158
171
|
const skillSnapshot = (() => {
|
|
159
172
|
try {
|
|
160
173
|
const shellPaths = context.workspace?.shellPaths;
|
|
161
|
-
if (!shellPaths) return { count: 0, enabled: 0, items: [] };
|
|
174
|
+
if (!shellPaths) return { count: 0, enabled: 0, active: 0, bundleCount: 0, enabledBundleCount: 0, items: [], bundleItems: [] };
|
|
162
175
|
const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
|
|
163
176
|
return {
|
|
164
177
|
count: snapshot.skills.length,
|
|
165
178
|
enabled: snapshot.enabledSkills.length,
|
|
179
|
+
active: snapshot.activeSkills.length,
|
|
180
|
+
bundleCount: snapshot.bundles.length,
|
|
181
|
+
enabledBundleCount: snapshot.enabledBundles.length,
|
|
166
182
|
items: snapshot.skills.map(summarizeSkillItem),
|
|
183
|
+
bundleItems: snapshot.bundles.map(summarizeSkillBundleItem),
|
|
167
184
|
};
|
|
168
185
|
} catch {
|
|
169
|
-
return { count: 0, enabled: 0, items: [] };
|
|
186
|
+
return { count: 0, enabled: 0, active: 0, bundleCount: 0, enabledBundleCount: 0, items: [], bundleItems: [] };
|
|
170
187
|
}
|
|
171
188
|
})();
|
|
172
189
|
const routineSnapshot = (() => {
|
|
@@ -251,6 +268,8 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
251
268
|
enabledRoutineCount: routineSnapshot.enabled,
|
|
252
269
|
skillCount: skillSnapshot.count,
|
|
253
270
|
enabledSkillCount: skillSnapshot.enabled,
|
|
271
|
+
skillBundleCount: skillSnapshot.bundleCount,
|
|
272
|
+
enabledSkillBundleCount: skillSnapshot.enabledBundleCount,
|
|
254
273
|
activePersonaName: personaSnapshot.activeName,
|
|
255
274
|
readyChannelCount: channels.filter((channel) => channel.ready).length,
|
|
256
275
|
voiceProviderCount: voiceProviders.length,
|
|
@@ -274,6 +293,10 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
274
293
|
localRoutines: routineSnapshot.items,
|
|
275
294
|
localSkillCount: skillSnapshot.count,
|
|
276
295
|
enabledSkillCount: skillSnapshot.enabled,
|
|
296
|
+
localSkillBundleCount: skillSnapshot.bundleCount,
|
|
297
|
+
enabledSkillBundleCount: skillSnapshot.enabledBundleCount,
|
|
298
|
+
activeSkillCount: skillSnapshot.active,
|
|
299
|
+
localSkillBundles: skillSnapshot.bundleItems,
|
|
277
300
|
localSkills: skillSnapshot.items,
|
|
278
301
|
localPersonaCount: personaSnapshot.count,
|
|
279
302
|
activePersonaName: personaSnapshot.activeName,
|
|
@@ -128,6 +128,10 @@ export interface AgentWorkspaceRuntimeSnapshot {
|
|
|
128
128
|
readonly localRoutines: readonly AgentWorkspaceLocalLibraryItem[];
|
|
129
129
|
readonly localSkillCount: number;
|
|
130
130
|
readonly enabledSkillCount: number;
|
|
131
|
+
readonly localSkillBundleCount: number;
|
|
132
|
+
readonly enabledSkillBundleCount: number;
|
|
133
|
+
readonly activeSkillCount: number;
|
|
134
|
+
readonly localSkillBundles: readonly AgentWorkspaceLocalLibraryItem[];
|
|
131
135
|
readonly localSkills: readonly AgentWorkspaceLocalLibraryItem[];
|
|
132
136
|
readonly localPersonaCount: number;
|
|
133
137
|
readonly activePersonaName: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AgentSkillRegistry, type AgentSkillRecord } from '../../agent/skill-registry.ts';
|
|
1
|
+
import { AgentSkillRegistry, type AgentSkillBundleRecord, type AgentSkillRecord } from '../../agent/skill-registry.ts';
|
|
2
2
|
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
3
3
|
import { requireShellPaths } from './runtime-services.ts';
|
|
4
4
|
|
|
@@ -55,6 +55,11 @@ function summarizeSkill(skill: AgentSkillRecord): string {
|
|
|
55
55
|
return ` ${skill.id} ${enabled} ${skill.reviewState} ${skill.name} - ${skill.description}${tags}`;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function summarizeBundle(bundle: AgentSkillBundleRecord): string {
|
|
59
|
+
const enabled = bundle.enabled ? 'enabled' : 'disabled';
|
|
60
|
+
return ` ${bundle.id} ${enabled} ${bundle.reviewState} ${bundle.name} - ${bundle.description} skills=${bundle.skillIds.join(',')}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
function renderList(title: string, registry: AgentSkillRegistry, skills: readonly AgentSkillRecord[]): string {
|
|
59
64
|
const snapshot = registry.snapshot();
|
|
60
65
|
if (skills.length === 0) {
|
|
@@ -68,6 +73,20 @@ function renderList(title: string, registry: AgentSkillRegistry, skills: readonl
|
|
|
68
73
|
].join('\n');
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
function renderBundleList(title: string, registry: AgentSkillRegistry, bundles: readonly AgentSkillBundleRecord[]): string {
|
|
77
|
+
const snapshot = registry.snapshot();
|
|
78
|
+
if (bundles.length === 0) {
|
|
79
|
+
return `${title}\n No local Agent skill bundles yet. Create one with /agent-skills bundle create --name <name> --description <summary> --skills <id,id>.`;
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
`${title} (${bundles.length})`,
|
|
83
|
+
` store: ${snapshot.path}`,
|
|
84
|
+
` enabled bundles: ${snapshot.enabledBundles.length}`,
|
|
85
|
+
` active skills: ${snapshot.activeSkills.length}`,
|
|
86
|
+
...bundles.map(summarizeBundle),
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
function renderSkill(skill: AgentSkillRecord): string {
|
|
72
91
|
return [
|
|
73
92
|
`Skill ${skill.name}`,
|
|
@@ -88,14 +107,143 @@ function renderSkill(skill: AgentSkillRecord): string {
|
|
|
88
107
|
].filter(Boolean).join('\n');
|
|
89
108
|
}
|
|
90
109
|
|
|
110
|
+
function renderBundle(bundle: AgentSkillBundleRecord, registry: AgentSkillRegistry): string {
|
|
111
|
+
const skills = bundle.skillIds
|
|
112
|
+
.map((skillId) => registry.get(skillId))
|
|
113
|
+
.filter((skill): skill is AgentSkillRecord => skill !== null);
|
|
114
|
+
return [
|
|
115
|
+
`Skill Bundle ${bundle.name}`,
|
|
116
|
+
` id: ${bundle.id}`,
|
|
117
|
+
` enabled: ${bundle.enabled ? 'yes' : 'no'}`,
|
|
118
|
+
` review: ${bundle.reviewState}`,
|
|
119
|
+
` source: ${bundle.source}`,
|
|
120
|
+
` provenance: ${bundle.provenance}`,
|
|
121
|
+
` skills: ${bundle.skillIds.join(', ')}`,
|
|
122
|
+
` created: ${bundle.createdAt}`,
|
|
123
|
+
` updated: ${bundle.updatedAt}`,
|
|
124
|
+
bundle.staleReason ? ` stale reason: ${bundle.staleReason}` : '',
|
|
125
|
+
'',
|
|
126
|
+
bundle.description,
|
|
127
|
+
'',
|
|
128
|
+
...skills.map((skill) => `- ${skill.id}: ${skill.name} - ${skill.description}`),
|
|
129
|
+
].filter(Boolean).join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
91
132
|
function printError(ctx: CommandContext, error: unknown): void {
|
|
92
133
|
ctx.print(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
93
134
|
}
|
|
94
135
|
|
|
136
|
+
function runBundleCommand(args: readonly string[], ctx: CommandContext, skillRegistry: AgentSkillRegistry): void {
|
|
137
|
+
const sub = (args[0] ?? 'list').toLowerCase();
|
|
138
|
+
if (sub === 'list' || sub === 'open') {
|
|
139
|
+
ctx.print(renderBundleList('Agent Skill Bundles', skillRegistry, skillRegistry.listBundles()));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (sub === 'enabled') {
|
|
143
|
+
const snapshot = skillRegistry.snapshot();
|
|
144
|
+
ctx.print(renderBundleList('Enabled Agent Skill Bundles', skillRegistry, snapshot.enabledBundles));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (sub === 'search') {
|
|
148
|
+
const query = args.slice(1).join(' ').trim();
|
|
149
|
+
ctx.print(renderBundleList(query ? `Agent Skill Bundles matching "${query}"` : 'Agent Skill Bundles', skillRegistry, skillRegistry.searchBundles(query)));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (sub === 'show') {
|
|
153
|
+
const id = args[1];
|
|
154
|
+
if (!id) {
|
|
155
|
+
ctx.print('Usage: /agent-skills bundle show <id>');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const bundle = skillRegistry.getBundle(id);
|
|
159
|
+
ctx.print(bundle ? renderBundle(bundle, skillRegistry) : `Unknown Agent skill bundle: ${id}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (sub === 'create') {
|
|
163
|
+
const parsed = parseSkillArgs(args.slice(1));
|
|
164
|
+
const bundle = skillRegistry.createBundle({
|
|
165
|
+
name: requiredFlag(parsed.flags, 'name'),
|
|
166
|
+
description: requiredFlag(parsed.flags, 'description'),
|
|
167
|
+
skillIds: splitList(requiredFlag(parsed.flags, 'skills')),
|
|
168
|
+
enabled: parsed.flags.get('enabled') === 'true',
|
|
169
|
+
source: 'user',
|
|
170
|
+
provenance: 'slash-command',
|
|
171
|
+
});
|
|
172
|
+
ctx.print(`Created Agent skill bundle ${bundle.id}: ${bundle.name}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (sub === 'update') {
|
|
176
|
+
const id = args[1];
|
|
177
|
+
if (!id) {
|
|
178
|
+
ctx.print('Usage: /agent-skills bundle update <id> [--name ...] [--description ...] [--skills id,id]');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const parsed = parseSkillArgs(args.slice(2));
|
|
182
|
+
const updated = skillRegistry.updateBundle(id, {
|
|
183
|
+
name: parsed.flags.get('name'),
|
|
184
|
+
description: parsed.flags.get('description'),
|
|
185
|
+
skillIds: parsed.flags.has('skills') ? splitList(parsed.flags.get('skills')) : undefined,
|
|
186
|
+
provenance: 'slash-command',
|
|
187
|
+
});
|
|
188
|
+
ctx.print(`Updated Agent skill bundle ${updated.id}: ${updated.name}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (sub === 'enable' || sub === 'disable') {
|
|
192
|
+
const id = args[1];
|
|
193
|
+
if (!id) {
|
|
194
|
+
ctx.print(`Usage: /agent-skills bundle ${sub} <id>`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const bundle = skillRegistry.setBundleEnabled(id, sub === 'enable');
|
|
198
|
+
ctx.print(`${sub === 'enable' ? 'Enabled' : 'Disabled'} Agent skill bundle ${bundle.id}: ${bundle.name}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (sub === 'review') {
|
|
202
|
+
const id = args[1];
|
|
203
|
+
if (!id) {
|
|
204
|
+
ctx.print('Usage: /agent-skills bundle review <id>');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const bundle = skillRegistry.markBundleReviewed(id);
|
|
208
|
+
ctx.print(`Reviewed Agent skill bundle ${bundle.id}.`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (sub === 'stale') {
|
|
212
|
+
const id = args[1];
|
|
213
|
+
if (!id) {
|
|
214
|
+
ctx.print('Usage: /agent-skills bundle stale <id> <reason...>');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const bundle = skillRegistry.markBundleStale(id, args.slice(2).join(' '));
|
|
218
|
+
ctx.print(`Marked Agent skill bundle ${bundle.id} stale.`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (sub === 'delete' || sub === 'remove') {
|
|
222
|
+
const parsed = parseSkillArgs(args.slice(1));
|
|
223
|
+
const id = parsed.rest[0];
|
|
224
|
+
if (!id) {
|
|
225
|
+
ctx.print('Usage: /agent-skills bundle delete <id> --yes');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!parsed.yes) {
|
|
229
|
+
ctx.print(`Refusing to delete Agent skill bundle ${id} without --yes.`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const removed = skillRegistry.deleteBundle(id);
|
|
233
|
+
ctx.print(`Deleted Agent skill bundle ${removed.id}: ${removed.name}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
ctx.print('Usage: /agent-skills bundle [list|enabled|search|show|create|update|enable|disable|review|stale|delete]');
|
|
237
|
+
}
|
|
238
|
+
|
|
95
239
|
export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
|
|
96
240
|
const sub = (args[0] ?? 'list').toLowerCase();
|
|
97
241
|
const skillRegistry = registryFromContext(ctx);
|
|
98
242
|
try {
|
|
243
|
+
if (sub === 'bundle' || sub === 'bundles') {
|
|
244
|
+
runBundleCommand(args.slice(1), ctx, skillRegistry);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
99
247
|
if (sub === 'list' || sub === 'open') {
|
|
100
248
|
ctx.print(renderList('Agent Skills', skillRegistry, skillRegistry.list()));
|
|
101
249
|
return;
|
|
@@ -199,7 +347,7 @@ export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx:
|
|
|
199
347
|
ctx.print(`Deleted Agent skill ${removed.id}: ${removed.name}`);
|
|
200
348
|
return;
|
|
201
349
|
}
|
|
202
|
-
ctx.print('Usage: /agent-skills [list|enabled|search|show|create|update|enable|disable|review|stale|delete]');
|
|
350
|
+
ctx.print('Usage: /agent-skills [list|enabled|search|show|create|update|enable|disable|review|stale|delete|bundle]');
|
|
203
351
|
} catch (error) {
|
|
204
352
|
printError(ctx, error);
|
|
205
353
|
}
|
|
@@ -210,7 +358,7 @@ export function registerAgentSkillsRuntimeCommands(registry: CommandRegistry): v
|
|
|
210
358
|
name: 'agent-skills',
|
|
211
359
|
aliases: ['askills', 'local-skills'],
|
|
212
360
|
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]',
|
|
361
|
+
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|bundle ...]',
|
|
214
362
|
handler: runAgentSkillsRuntimeCommand,
|
|
215
363
|
});
|
|
216
364
|
}
|