feat: add in-chat skill discovery and local registry install command
This commit is contained in:
@@ -474,6 +474,7 @@ Notes:
|
|||||||
| `/approvals` | List pending approval gates for current session |
|
| `/approvals` | List pending approval gates for current session |
|
||||||
| `/approve [id]` | Approve latest (or specific) pending gate |
|
| `/approve [id]` | Approve latest (or specific) pending gate |
|
||||||
| `/deny [id] [reason]` | Deny latest (or specific) pending gate |
|
| `/deny [id] [reason]` | Deny latest (or specific) pending gate |
|
||||||
|
| `/skill <list|search|install>` | In-chat skill discovery/install (`list`, `search <term>`, `install <registry-id>`) |
|
||||||
|
|
||||||
## Web UI Dashboard
|
## Web UI Dashboard
|
||||||
|
|
||||||
@@ -524,6 +525,7 @@ pnpm tui:fs
|
|||||||
| `/approvals` | List pending approval gates for current session |
|
| `/approvals` | List pending approval gates for current session |
|
||||||
| `/approve [id]` | Approve latest (or specific) pending gate |
|
| `/approve [id]` | Approve latest (or specific) pending gate |
|
||||||
| `/deny [id] [reason]` | Deny latest (or specific) pending gate |
|
| `/deny [id] [reason]` | Deny latest (or specific) pending gate |
|
||||||
|
| `/skill <list|search|install>` | In-chat skill discovery/install (`list`, `search <term>`, `install <registry-id>`) |
|
||||||
| `/quit` | Exit |
|
| `/quit` | Exit |
|
||||||
|
|
||||||
#### Runtime Model Switching
|
#### Runtime Model Switching
|
||||||
|
|||||||
+19
-1
@@ -5305,6 +5305,24 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/hooks/engine.test.ts src/commands/builtin/index.test.ts src/daemon/routing.test.ts src/tools/executor.test.ts + pnpm typecheck passing"
|
"test_status": "pnpm test:run src/hooks/engine.test.ts src/commands/builtin/index.test.ts src/daemon/routing.test.ts src/tools/executor.test.ts + pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"skill-discovery-in-chat-tier-b4": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-18",
|
||||||
|
"updated": "2026-02-18",
|
||||||
|
"summary": "Completed Tier B4 in-chat discovery/install surface by adding `/skill` command (`list|search|install`) through command fast-path. Reused existing registry source/catalog infrastructure and managed-skill installer, with local registry-source install support and explicit remote-source fallback guidance to CLI.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/commands/types.ts",
|
||||||
|
"src/commands/builtin/index.ts",
|
||||||
|
"src/commands/builtin/index.test.ts",
|
||||||
|
"src/commands/index.ts",
|
||||||
|
"src/daemon/routing.ts",
|
||||||
|
"src/daemon/routing.test.ts",
|
||||||
|
"src/daemon/index.ts",
|
||||||
|
"README.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/commands/builtin/index.test.ts src/daemon/routing.test.ts + pnpm typecheck passing"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
@@ -5328,7 +5346,7 @@
|
|||||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
||||||
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback",
|
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback",
|
||||||
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
||||||
"next_up": "Implement Tier B4 skill discovery index (registry-backed search/install flow)"
|
"next_up": "Implement Tier B3 progressive web app push notifications for WebChat"
|
||||||
},
|
},
|
||||||
"soul_md_and_cron_create": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { createApproveCommand, createApprovalsCommand, createContextCommand, createDenyCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createTransferCommand } from './index.js';
|
import { createApproveCommand, createApprovalsCommand, createContextCommand, createDenyCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createSkillCommand, createTransferCommand } from './index.js';
|
||||||
|
|
||||||
describe('builtin /model command', () => {
|
describe('builtin /model command', () => {
|
||||||
it('passes through the full argument string', async () => {
|
it('passes through the full argument string', async () => {
|
||||||
@@ -241,3 +241,19 @@ describe('builtin approval commands', () => {
|
|||||||
expect(result).toEqual({ handled: true, text: 'denied' });
|
expect(result).toEqual({ handled: true, text: 'denied' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('builtin /skill command', () => {
|
||||||
|
it('passes subcommand text to skillCommand service', async () => {
|
||||||
|
const cmd = createSkillCommand();
|
||||||
|
const skillCommand = vi.fn(() => 'ok');
|
||||||
|
const result = await cmd.execute(['search', 'calendar'], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/skill search calendar',
|
||||||
|
services: { skillCommand },
|
||||||
|
});
|
||||||
|
expect(skillCommand).toHaveBeenCalledWith('search calendar');
|
||||||
|
expect(result).toEqual({ handled: true, text: 'ok' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -284,6 +284,22 @@ export function createDenyCommand(): CommandDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createSkillCommand(): CommandDefinition {
|
||||||
|
return {
|
||||||
|
name: 'skill',
|
||||||
|
description: 'In-chat skill discovery and install (list/search/install)',
|
||||||
|
execute: async (args, ctx) => {
|
||||||
|
if (!ctx.services?.skillCommand) {
|
||||||
|
return notAvailable('Skill command');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
text: await ctx.services.skillCommand(args.join(' ').trim()),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
||||||
registry.register(createHelpCommand(registry));
|
registry.register(createHelpCommand(registry));
|
||||||
registry.register(createStatusCommand());
|
registry.register(createStatusCommand());
|
||||||
@@ -299,4 +315,5 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|||||||
registry.register(createApprovalsCommand());
|
registry.register(createApprovalsCommand());
|
||||||
registry.register(createApproveCommand());
|
registry.register(createApproveCommand());
|
||||||
registry.register(createDenyCommand());
|
registry.register(createDenyCommand());
|
||||||
|
registry.register(createSkillCommand());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ export {
|
|||||||
createApprovalsCommand,
|
createApprovalsCommand,
|
||||||
createApproveCommand,
|
createApproveCommand,
|
||||||
createDenyCommand,
|
createDenyCommand,
|
||||||
|
createSkillCommand,
|
||||||
registerBuiltinCommands,
|
registerBuiltinCommands,
|
||||||
} from './builtin/index.js';
|
} from './builtin/index.js';
|
||||||
|
|||||||
@@ -38,4 +38,5 @@ export interface CommandServices {
|
|||||||
getApprovals?: () => Promise<string> | string;
|
getApprovals?: () => Promise<string> | string;
|
||||||
approvePending?: (input: string) => Promise<string> | string;
|
approvePending?: (input: string) => Promise<string> | string;
|
||||||
denyPending?: (input: string) => Promise<string> | string;
|
denyPending?: (input: string) => Promise<string> | string;
|
||||||
|
skillCommand?: (input: string) => Promise<string> | string;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -213,7 +213,7 @@ export async function startDaemon(config: Config, options?: StartDaemonOptions):
|
|||||||
|
|
||||||
const messageRouter = createMessageRouter({
|
const messageRouter = createMessageRouter({
|
||||||
sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor,
|
sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor,
|
||||||
config, memoryStore, agentConfigRegistry, agentRouter, sandboxManager, commandRegistry, hookEngine, intentRegistry, routingPolicy, skillRegistry,
|
config, memoryStore, agentConfigRegistry, agentRouter, sandboxManager, commandRegistry, hookEngine, intentRegistry, routingPolicy, skillRegistry, skillInstaller,
|
||||||
...createConfiguredExternalBackends(config),
|
...createConfiguredExternalBackends(config),
|
||||||
});
|
});
|
||||||
channelRegistry.setMessageHandler(messageRouter.handler);
|
channelRegistry.setMessageHandler(messageRouter.handler);
|
||||||
|
|||||||
@@ -911,6 +911,79 @@ describe('daemon command fast-path integration', () => {
|
|||||||
await expect(pendingPromise).resolves.toEqual({ approved: true });
|
await expect(pendingPromise).resolves.toEqual({ approved: true });
|
||||||
expect(processSpy).not.toHaveBeenCalled();
|
expect(processSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles /skill list via command fast-path', async () => {
|
||||||
|
const processSpy = vi.spyOn(AgentOrchestrator.prototype, 'process');
|
||||||
|
const session = {
|
||||||
|
id: 'telegram:skill-user',
|
||||||
|
addMessage: vi.fn(),
|
||||||
|
getHistory: vi.fn(() => []),
|
||||||
|
clear: vi.fn(),
|
||||||
|
replaceHistory: vi.fn(),
|
||||||
|
getConfig: vi.fn(() => undefined),
|
||||||
|
setConfig: vi.fn(),
|
||||||
|
deleteConfig: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandRegistry = new CommandRegistry();
|
||||||
|
registerBuiltinCommands(commandRegistry);
|
||||||
|
|
||||||
|
const router = createMessageRouter({
|
||||||
|
sessionManager: {
|
||||||
|
getSession: vi.fn(() => session),
|
||||||
|
} as unknown as MessageRouterDeps['sessionManager'],
|
||||||
|
modelRouter: {
|
||||||
|
getAvailableTiers: () => ['fast', 'default', 'complex', 'local'],
|
||||||
|
getAllLabels: () => ({ fast: 'fast', default: 'default', complex: 'complex', local: 'local' }),
|
||||||
|
getLabel: (tier: string) => tier,
|
||||||
|
} as unknown as MessageRouterDeps['modelRouter'],
|
||||||
|
systemPrompt: 'test prompt',
|
||||||
|
toolRegistry: {
|
||||||
|
clone() { return this; },
|
||||||
|
register: vi.fn(),
|
||||||
|
} as unknown as MessageRouterDeps['toolRegistry'],
|
||||||
|
toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'],
|
||||||
|
config: {
|
||||||
|
agents: {
|
||||||
|
primary_tier: 'default',
|
||||||
|
delegation: {
|
||||||
|
compaction: 'fast',
|
||||||
|
memory_extraction: 'fast',
|
||||||
|
classification: 'fast',
|
||||||
|
tool_summarisation: 'fast',
|
||||||
|
complex_reasoning: 'complex',
|
||||||
|
},
|
||||||
|
max_delegation_depth: 3,
|
||||||
|
max_iterations: 10,
|
||||||
|
},
|
||||||
|
compaction: { enabled: false },
|
||||||
|
models: { default: { provider: 'anthropic', model: 'claude' } },
|
||||||
|
} as unknown as MessageRouterDeps['config'],
|
||||||
|
commandRegistry,
|
||||||
|
skillRegistry: {
|
||||||
|
listAvailable: () => ([
|
||||||
|
{ manifest: { name: 'calendar', tier: 'managed', version: '1.2.0' } },
|
||||||
|
{ manifest: { name: 'todoist', tier: 'workspace', version: '0.4.1' } },
|
||||||
|
]),
|
||||||
|
} as unknown as MessageRouterDeps['skillRegistry'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const reply = vi.fn(async (_message: OutboundMessage) => {});
|
||||||
|
await router.handler({
|
||||||
|
id: 'skill-1',
|
||||||
|
channel: 'telegram',
|
||||||
|
senderId: 'skill-user',
|
||||||
|
text: '/skill list',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
metadata: { isCommand: true, command: 'skill', commandArgs: 'list' },
|
||||||
|
} as MessageRouterInput, reply);
|
||||||
|
|
||||||
|
expect(processSpy).not.toHaveBeenCalled();
|
||||||
|
const outbound = reply.mock.calls[0]?.[0] as OutboundMessage | undefined;
|
||||||
|
expect(String(outbound?.text)).toContain('Available skills (2):');
|
||||||
|
expect(String(outbound?.text)).toContain('calendar');
|
||||||
|
expect(String(outbound?.text)).toContain('todoist');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('daemon external backend integration', () => {
|
describe('daemon external backend integration', () => {
|
||||||
|
|||||||
+136
-1
@@ -23,9 +23,11 @@ import type { RoutingPolicy } from '../routing/index.js';
|
|||||||
import type { HookEngine } from '../hooks/index.js';
|
import type { HookEngine } from '../hooks/index.js';
|
||||||
import { createClientFromConfig } from './models.js';
|
import { createClientFromConfig } from './models.js';
|
||||||
import { matchReactionPrompt } from '../automation/reactions.js';
|
import { matchReactionPrompt } from '../automation/reactions.js';
|
||||||
import type { SkillRegistry } from '../skills/index.js';
|
import { loadSkillRegistryCatalog } from '../skills/index.js';
|
||||||
|
import type { SkillInstaller, SkillRegistry, SkillRegistryEntry, SkillRegistrySource } from '../skills/index.js';
|
||||||
import { auditLogger } from '../audit/index.js';
|
import { auditLogger } from '../audit/index.js';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
import { dirname, resolve } from 'path';
|
||||||
|
|
||||||
function buildProviderConfigMap(config: Config): Partial<Record<ModelProvider, ModelConfig>> {
|
function buildProviderConfigMap(config: Config): Partial<Record<ModelProvider, ModelConfig>> {
|
||||||
const providerConfigs: Partial<Record<ModelProvider, ModelConfig>> = {};
|
const providerConfigs: Partial<Record<ModelProvider, ModelConfig>> = {};
|
||||||
@@ -98,6 +100,47 @@ function isTtsEnabledForChannel(config: Config, channel: string): boolean {
|
|||||||
}
|
}
|
||||||
return enabledChannels.includes(channel);
|
return enabledChannels.includes(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveRegistrySource(config: Config): { source?: SkillRegistrySource; error?: string } {
|
||||||
|
const raw = config.skills.registry_source?.trim() || process.env.FLYNN_SKILLS_REGISTRY_SOURCE?.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return {
|
||||||
|
error: 'Skills registry is not configured. Set `skills.registry_source` (or FLYNN_SKILLS_REGISTRY_SOURCE).',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (raw.startsWith('http://')) {
|
||||||
|
return { error: `Registry URL must use https:// (${raw})` };
|
||||||
|
}
|
||||||
|
if (raw.startsWith('https://')) {
|
||||||
|
return { source: { type: 'url', url: raw } };
|
||||||
|
}
|
||||||
|
return { source: { type: 'file', path: raw } };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRegistryEntryLocalPath(entry: SkillRegistryEntry, registrySource: SkillRegistrySource): string | null {
|
||||||
|
const source = entry.source.trim();
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.startsWith('http://') || source.startsWith('https://') || source.startsWith('git+https://')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.startsWith('file://')) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(new URL(source).pathname);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registrySource.type === 'file' && (source.startsWith('./') || source.startsWith('../'))) {
|
||||||
|
return resolve(dirname(resolve(registrySource.path)), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(source);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Create the unified message handler for the channel registry.
|
* Create the unified message handler for the channel registry.
|
||||||
* Each channel+sender pair gets its own AgentOrchestrator backed by a persistent session.
|
* Each channel+sender pair gets its own AgentOrchestrator backed by a persistent session.
|
||||||
@@ -121,6 +164,7 @@ export function createMessageRouter(deps: {
|
|||||||
intentRegistry?: ComponentRegistry;
|
intentRegistry?: ComponentRegistry;
|
||||||
routingPolicy?: RoutingPolicy;
|
routingPolicy?: RoutingPolicy;
|
||||||
skillRegistry?: SkillRegistry;
|
skillRegistry?: SkillRegistry;
|
||||||
|
skillInstaller?: SkillInstaller;
|
||||||
externalBackends?: Partial<Record<ExternalBackendName, ExternalBackend>>;
|
externalBackends?: Partial<Record<ExternalBackendName, ExternalBackend>>;
|
||||||
defaultName?: ExternalBackendName;
|
defaultName?: ExternalBackendName;
|
||||||
}): {
|
}): {
|
||||||
@@ -1018,6 +1062,97 @@ export function createMessageRouter(deps: {
|
|||||||
? `Denied: ${selected.tool} (${selected.id}) — ${reason}`
|
? `Denied: ${selected.tool} (${selected.id}) — ${reason}`
|
||||||
: `Approval request is no longer pending: ${selected.id}`;
|
: `Approval request is no longer pending: ${selected.id}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
skillCommand: async (inputRaw: string) => {
|
||||||
|
const input = inputRaw.trim();
|
||||||
|
const [actionRaw, ...rest] = input.split(/\s+/).filter(Boolean);
|
||||||
|
const action = actionRaw?.toLowerCase();
|
||||||
|
|
||||||
|
if (!action || action === 'help') {
|
||||||
|
return [
|
||||||
|
'Usage: /skill <list|search|install>',
|
||||||
|
'/skill list',
|
||||||
|
'/skill search <term>',
|
||||||
|
'/skill install <registry-id>',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'list') {
|
||||||
|
const skills = deps.skillRegistry?.listAvailable() ?? [];
|
||||||
|
if (skills.length === 0) {
|
||||||
|
return 'No available skills are currently loaded.';
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`Available skills (${skills.length}):`,
|
||||||
|
...skills.map((skill) => `- ${skill.manifest.name} (${skill.manifest.tier}, v${skill.manifest.version})`),
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'search') {
|
||||||
|
const term = rest.join(' ').trim().toLowerCase();
|
||||||
|
if (!term) {
|
||||||
|
return 'Usage: /skill search <term>';
|
||||||
|
}
|
||||||
|
const sourceResolved = resolveRegistrySource(deps.config);
|
||||||
|
if (!sourceResolved.source) {
|
||||||
|
return sourceResolved.error ?? 'Failed to resolve registry source.';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const catalog = await loadSkillRegistryCatalog(sourceResolved.source);
|
||||||
|
const matches = catalog.skills.filter((entry) => {
|
||||||
|
const haystack = `${entry.id} ${entry.name} ${entry.summary} ${entry.publisher ?? ''}`.toLowerCase();
|
||||||
|
return haystack.includes(term);
|
||||||
|
}).slice(0, 12);
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return `No registry skills found for "${term}".`;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`Registry matches (${matches.length}):`,
|
||||||
|
...matches.map((entry) => `- ${entry.id} (${entry.version}) — ${entry.summary}`),
|
||||||
|
].join('\n');
|
||||||
|
} catch (error) {
|
||||||
|
return `Registry lookup failed: ${error instanceof Error ? error.message : String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'install') {
|
||||||
|
const registryId = rest.join(' ').trim().toLowerCase();
|
||||||
|
if (!registryId) {
|
||||||
|
return 'Usage: /skill install <registry-id>';
|
||||||
|
}
|
||||||
|
if (!deps.skillInstaller || !deps.skillRegistry) {
|
||||||
|
return 'Skill installation is unavailable in this runtime.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceResolved = resolveRegistrySource(deps.config);
|
||||||
|
if (!sourceResolved.source) {
|
||||||
|
return sourceResolved.error ?? 'Failed to resolve registry source.';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const catalog = await loadSkillRegistryCatalog(sourceResolved.source);
|
||||||
|
const entry = catalog.skills.find((item) => item.id.toLowerCase() === registryId);
|
||||||
|
if (!entry) {
|
||||||
|
return `Registry skill not found: ${registryId}`;
|
||||||
|
}
|
||||||
|
const localPath = resolveRegistryEntryLocalPath(entry, sourceResolved.source);
|
||||||
|
if (!localPath) {
|
||||||
|
return `Registry entry '${entry.id}' points to a remote source. Use CLI install for remote sources.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installed = deps.skillInstaller.install(localPath);
|
||||||
|
if (!installed) {
|
||||||
|
return `Failed to install skill from ${entry.source}`;
|
||||||
|
}
|
||||||
|
deps.skillRegistry.register(installed);
|
||||||
|
return `Installed skill '${installed.manifest.name}' (${installed.manifest.version}) from registry id '${entry.id}'.`;
|
||||||
|
} catch (error) {
|
||||||
|
return `Skill install failed: ${error instanceof Error ? error.message : String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown skill action. Use: list, search, install';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user