Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions packages/cli/src/config/extension-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,9 +891,10 @@ Would you like to attempt to install via "git clone" instead?`,
let skills = await loadSkillsFromDir(
path.join(effectiveExtensionPath, 'skills'),
);
skills = skills.map((skill) =>
recursivelyHydrateStrings(skill, hydrationContext),
);
skills = skills.map((skill) => ({
...recursivelyHydrateStrings(skill, hydrationContext),
extensionName: config.name,
}));

let rules: PolicyRule[] | undefined;
let checkers: SafetyCheckerRule[] | undefined;
Expand All @@ -916,9 +917,10 @@ Would you like to attempt to install via "git clone" instead?`,
const agentLoadResult = await loadAgentsFromDirectory(
path.join(effectiveExtensionPath, 'agents'),
);
agentLoadResult.agents = agentLoadResult.agents.map((agent) =>
recursivelyHydrateStrings(agent, hydrationContext),
);
agentLoadResult.agents = agentLoadResult.agents.map((agent) => ({
...recursivelyHydrateStrings(agent, hydrationContext),
extensionName: config.name,
}));

// Log errors but don't fail the entire extension load
for (const error of agentLoadResult.errors) {
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/services/SkillCommandLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,16 @@ describe('SkillCommandLoader', () => {
const actionResult = (await commands[0].action!({} as any, '')) as any;
expect(actionResult.toolArgs).toEqual({ name: 'my awesome skill' });
});

it('should propagate extensionName to the generated slash command', async () => {
const mockSkills = [
{ name: 'skill1', description: 'desc', extensionName: 'ext1' },
];
mockSkillManager.getDisplayableSkills.mockReturnValue(mockSkills);

const loader = new SkillCommandLoader(mockConfig);
const commands = await loader.loadCommands(new AbortController().signal);

expect(commands[0].extensionName).toBe('ext1');
});
});
1 change: 1 addition & 0 deletions packages/cli/src/services/SkillCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class SkillCommandLoader implements ICommandLoader {
description: skill.description || `Activate the ${skill.name} skill`,
kind: CommandKind.SKILL,
autoExecute: true,
extensionName: skill.extensionName,
action: async (_context, args) => ({
type: 'tool',
toolName: ACTIVATE_SKILL_TOOL_NAME,
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/src/services/SlashCommandConflictHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,23 @@ describe('SlashCommandConflictHandler', () => {
vi.advanceTimersByTime(600);
expect(coreEvents.emitFeedback).not.toHaveBeenCalled();
});

it('should display a descriptive message for a skill conflict', () => {
simulateEvent([
{
name: 'chat',
renamedTo: 'google-workspace.chat',
loserExtensionName: 'google-workspace',
loserKind: CommandKind.SKILL,
winnerKind: CommandKind.BUILT_IN,
},
]);

vi.advanceTimersByTime(600);

expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'info',
"Extension 'google-workspace' skill '/chat' was renamed to '/google-workspace.chat' because it conflicts with built-in command.",
);
});
});
4 changes: 4 additions & 0 deletions packages/cli/src/services/SlashCommandConflictHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ export class SlashCommandConflictHandler {
return extensionName
? `extension '${extensionName}' command`
: 'extension command';
case CommandKind.SKILL:
return extensionName
? `extension '${extensionName}' skill`
: 'skill command';
case CommandKind.MCP_PROMPT:
return mcpServerName
? `MCP server '${mcpServerName}' command`
Expand Down
25 changes: 25 additions & 0 deletions packages/cli/src/services/SlashCommandResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,30 @@ describe('SlashCommandResolver', () => {

expect(finalCommands.find((c) => c.name === 'gcp.deploy1')).toBeDefined();
});

it('should prefix skills with extension name when they conflict with built-in', () => {
const builtin = createMockCommand('chat', CommandKind.BUILT_IN);
const skill = {
...createMockCommand('chat', CommandKind.SKILL),
extensionName: 'google-workspace',
};

const { finalCommands } = SlashCommandResolver.resolve([builtin, skill]);

const names = finalCommands.map((c) => c.name);
expect(names).toContain('chat');
expect(names).toContain('google-workspace.chat');
});

it('should NOT prefix skills with "skill" when extension name is missing', () => {
const builtin = createMockCommand('chat', CommandKind.BUILT_IN);
const skill = createMockCommand('chat', CommandKind.SKILL);

const { finalCommands } = SlashCommandResolver.resolve([builtin, skill]);

const names = finalCommands.map((c) => c.name);
expect(names).toContain('chat');
expect(names).toContain('chat1');
});
});
});
2 changes: 1 addition & 1 deletion packages/cli/src/services/SlashCommandResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export class SlashCommandResolver {
private static getPrefix(cmd: SlashCommand): string | undefined {
switch (cmd.kind) {
case CommandKind.EXTENSION_FILE:
case CommandKind.SKILL:
return cmd.extensionName;
case CommandKind.MCP_PROMPT:
return cmd.mcpServerName;
Expand All @@ -185,7 +186,6 @@ export class SlashCommandResolver {
return undefined;
}
}

/**
* Logs a conflict event.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/hooks/slashCommandProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,9 @@ export const useSlashCommandProcessor = (
(async () => {
const commandService = await CommandService.create(
[
new BuiltinCommandLoader(config),
new SkillCommandLoader(config),
new McpPromptLoader(config),
new BuiltinCommandLoader(config),
new FileCommandLoader(config),
],
controller.signal,
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/code_assist/oauth2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ describe('oauth2', () => {
expect(fs.existsSync(googleAccountPath)).toBe(true);
if (fs.existsSync(googleAccountPath)) {
const cachedGoogleAccount = fs.readFileSync(googleAccountPath, 'utf-8');

expect(JSON.parse(cachedGoogleAccount)).toEqual({
active: 'test-user-code-account@gmail.com',
old: [],
Expand Down Expand Up @@ -1349,7 +1350,7 @@ describe('oauth2', () => {
let dataHandler: ((data: Buffer) => void) | undefined;
await vi.waitFor(() => {
const dataCall = stdinOnSpy.mock.calls.find(
(call: [string, ...unknown[]]) => call[0] === 'data',
(call: [string | symbol, ...unknown[]]) => call[0] === 'data',
);
dataHandler = dataCall?.[1] as ((data: Buffer) => void) | undefined;
if (!dataHandler) throw new Error('stdin handler not registered yet');
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/skills/skillLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface SkillDefinition {
disabled?: boolean;
/** Whether the skill is a built-in skill. */
isBuiltin?: boolean;
/** The name of the extension that provided this skill, if any. */
extensionName?: string;
}

export const FRONTMATTER_REGEX =
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/telemetry/memory-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const mockHeapStatistics = {
total_global_handles_size: 8192,
used_global_handles_size: 4096,
external_memory: 2097152,
total_allocated_bytes: 31457280,
};

const mockHeapSpaceStatistics = [
Expand Down
Loading