@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.
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
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 localUserAuthManager = createCliLocalUserAuthManager(runtime);
588
- const [sub = 'status', ...rawRest] = runtime.cli.commandArgs;
589
- const rest = commandValues(rawRest);
590
- if (sub === 'add-user' || sub === 'add') {
591
- const username = rest[0];
592
- if (!username) return 'Usage: goodvibes auth add-user <username> [--password <value>|--password-stdin] [--role <role>]';
593
- const password = readPassword(rawRest);
594
- if (!password) return 'Usage: goodvibes auth add-user <username> [--password <value>|--password-stdin] [--role <role>]';
595
- const roles = readOptionValues(rawRest, '--role').filter((role) => role.length > 0);
596
- const user = localUserAuthManager.addUser(username, password, roles.length > 0 ? roles : ['user']);
597
- return `Auth user added: ${user.username} (${user.roles.join(', ') || 'no roles'})`;
598
- }
599
- if (sub === 'delete-user' || sub === 'remove-user') {
600
- const username = rest[0];
601
- if (!username) return 'Usage: goodvibes auth delete-user <username>';
602
- return localUserAuthManager.deleteUser(username)
603
- ? `Auth user deleted: ${username}`
604
- : `No auth user found: ${username}`;
605
- }
606
- if (sub === 'rotate-password' || sub === 'passwd') {
607
- const username = rest[0];
608
- if (!username) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
609
- const password = readPassword(rawRest);
610
- if (!password) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
611
- localUserAuthManager.rotatePassword(username, password);
612
- return `Auth password rotated: ${username}`;
613
- }
614
- if (sub === 'revoke-session') {
615
- const token = rest[0];
616
- if (!token) return 'Usage: goodvibes auth revoke-session <token-or-fingerprint>';
617
- return localUserAuthManager.revokeSession(token)
618
- ? 'Auth session revoked.'
619
- : 'No auth session found.';
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
- 'GoodVibes auth',
657
- ` permission mode: ${String(value.permissionMode)}`,
658
- ` users: ${value.users.length}`,
659
- ...value.users.map((user) => ` ${user.username} (${user.roles.join(', ') || 'no roles'})`),
660
- ` sessions: ${value.sessions}`,
661
- ` user store: ${paths.userStorePresent ? 'present' : 'missing'} (${paths.userStorePath})`,
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 localUsers: CliAuthStatus | null;
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-local-users',
185
+ id: 'network-endpoint-without-runtime-auth-signal',
186
186
  area: 'auth',
187
187
  severity: 'risk',
188
- summary: 'Network-facing runtime endpoints are enabled before local users are configured.',
189
- cause: `${networkFacingSurfaces.map(([name]) => name).join(', ')} are LAN/custom-bound, but no local auth user store was found.`,
190
- impact: 'Remote access paths may be unusable or unsafe until local admin auth is configured.',
191
- action: 'Create/verify a local admin user before exposing GoodVibes on the network.',
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: 'Replace bootstrap auth with a named admin user and retire the bootstrap credential.',
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
- localUsers: options.auth ?? null,
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
- ? ` localUsers: ${options.auth.userStorePresent ? 'present' : 'missing'} (${options.auth.userStorePath})`
321
- : ' localUsers: unknown',
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: setupStatusForCount(input.enabledSkillCount, 'ready', input.skillCount > 0 ? 'recommended' : 'optional'),
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
  }