@meowlynxsea/koi 0.1.0
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/LICENSE +34 -0
- package/NOTICE +35 -0
- package/README.md +15 -0
- package/bin/koi +12 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/main.js +489918 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/package.json +51 -0
- package/src/agent/check-permissions.ts +239 -0
- package/src/agent/hooks/message-utils.ts +305 -0
- package/src/agent/hooks/types.ts +32 -0
- package/src/agent/hooks.ts +1560 -0
- package/src/agent/mode.ts +163 -0
- package/src/agent/monitor-registry.ts +308 -0
- package/src/agent/permission-ui.ts +71 -0
- package/src/agent/plan-ui.ts +74 -0
- package/src/agent/question-ui.ts +58 -0
- package/src/agent/session-fork.ts +299 -0
- package/src/agent/session-snapshots.ts +216 -0
- package/src/agent/session-store.ts +649 -0
- package/src/agent/session-tasks.ts +305 -0
- package/src/agent/session.ts +27 -0
- package/src/agent/subagent-registry.ts +176 -0
- package/src/agent/subagent.ts +194 -0
- package/src/agent/tool-orchestration.ts +55 -0
- package/src/agent/tools.ts +8 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/commands.ts +5 -0
- package/src/commands/skills/index.ts +23 -0
- package/src/config/models.ts +6 -0
- package/src/config/settings.ts +392 -0
- package/src/main.tsx +64 -0
- package/src/services/mcp/client.ts +194 -0
- package/src/services/mcp/config.ts +232 -0
- package/src/services/mcp/connection-manager.ts +258 -0
- package/src/services/mcp/index.ts +80 -0
- package/src/services/mcp/mcp-commands.ts +114 -0
- package/src/services/mcp/stdio-transport.ts +246 -0
- package/src/services/mcp/types.ts +155 -0
- package/src/skills/SkillsMenu.tsx +370 -0
- package/src/skills/bundled/batch.ts +106 -0
- package/src/skills/bundled/debug.ts +86 -0
- package/src/skills/bundled/loremIpsum.ts +101 -0
- package/src/skills/bundled/remember.ts +97 -0
- package/src/skills/bundled/simplify.ts +100 -0
- package/src/skills/bundled/skillify.ts +123 -0
- package/src/skills/bundled/stuck.ts +101 -0
- package/src/skills/bundled/updateConfig.ts +228 -0
- package/src/skills/bundled.ts +46 -0
- package/src/skills/frontmatter.ts +179 -0
- package/src/skills/index.ts +87 -0
- package/src/skills/invoke.ts +231 -0
- package/src/skills/loader.ts +710 -0
- package/src/skills/substitution.ts +169 -0
- package/src/skills/types.ts +201 -0
- package/src/tools/agent.ts +143 -0
- package/src/tools/ask-user-question.ts +46 -0
- package/src/tools/bash.ts +148 -0
- package/src/tools/edit.ts +164 -0
- package/src/tools/glob.ts +102 -0
- package/src/tools/grep.ts +248 -0
- package/src/tools/index.ts +73 -0
- package/src/tools/list-mcp-resources.ts +74 -0
- package/src/tools/ls.ts +85 -0
- package/src/tools/mcp.ts +76 -0
- package/src/tools/monitor.ts +159 -0
- package/src/tools/plan-mode.ts +134 -0
- package/src/tools/read-mcp-resource.ts +79 -0
- package/src/tools/read.ts +137 -0
- package/src/tools/skill.ts +176 -0
- package/src/tools/task.ts +349 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/webfetch-domains.ts +239 -0
- package/src/tools/webfetch.ts +533 -0
- package/src/tools/write.ts +101 -0
- package/src/tui/app.tsx +1178 -0
- package/src/tui/components/chat-panel.tsx +1071 -0
- package/src/tui/components/command-panel.tsx +261 -0
- package/src/tui/components/confirm-modal.tsx +135 -0
- package/src/tui/components/connect-modal.tsx +435 -0
- package/src/tui/components/connecting-modal.tsx +167 -0
- package/src/tui/components/edit-pending-modal.tsx +103 -0
- package/src/tui/components/exit-modal.tsx +131 -0
- package/src/tui/components/fork-modal.tsx +377 -0
- package/src/tui/components/image-preview-modal.tsx +141 -0
- package/src/tui/components/image-utils.ts +128 -0
- package/src/tui/components/info-bar.tsx +103 -0
- package/src/tui/components/input-box.tsx +352 -0
- package/src/tui/components/mcp/MCPSettings.tsx +386 -0
- package/src/tui/components/mcp/index.ts +7 -0
- package/src/tui/components/model-modal.tsx +310 -0
- package/src/tui/components/pending-area.tsx +88 -0
- package/src/tui/components/rename-modal.tsx +119 -0
- package/src/tui/components/session-modal.tsx +233 -0
- package/src/tui/components/side-bar.tsx +349 -0
- package/src/tui/components/tool-output.ts +6 -0
- package/src/tui/hooks/user-prompt-history.ts +114 -0
- package/src/tui/theme.ts +63 -0
- package/src/types/commands.ts +80 -0
- package/src/types/cross-spawn.d.ts +24 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontmatter Parser for SKILL.md files
|
|
3
|
+
*
|
|
4
|
+
* Parses YAML frontmatter from skill markdown files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import yaml from "yaml";
|
|
8
|
+
|
|
9
|
+
export interface RawFrontmatter {
|
|
10
|
+
// Allow any keys but also provide explicit accessors
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
name?: unknown;
|
|
13
|
+
description?: unknown;
|
|
14
|
+
Description?: unknown;
|
|
15
|
+
when_to_use?: unknown;
|
|
16
|
+
whenToUse?: unknown;
|
|
17
|
+
"argument-hint"?: unknown;
|
|
18
|
+
argumentHint?: unknown;
|
|
19
|
+
ArgumentHint?: unknown;
|
|
20
|
+
arguments?: unknown;
|
|
21
|
+
Arguments?: unknown;
|
|
22
|
+
"allowed-tools"?: unknown;
|
|
23
|
+
allowedTools?: unknown;
|
|
24
|
+
allowed_tools?: unknown;
|
|
25
|
+
model?: unknown;
|
|
26
|
+
Model?: unknown;
|
|
27
|
+
"disable-model-invocation"?: unknown;
|
|
28
|
+
disableModelInvocation?: unknown;
|
|
29
|
+
"user-invocable"?: unknown;
|
|
30
|
+
userInvocable?: unknown;
|
|
31
|
+
hooks?: unknown;
|
|
32
|
+
Hooks?: unknown;
|
|
33
|
+
context?: unknown;
|
|
34
|
+
Context?: unknown;
|
|
35
|
+
agent?: unknown;
|
|
36
|
+
Agent?: unknown;
|
|
37
|
+
effort?: unknown;
|
|
38
|
+
Effort?: unknown;
|
|
39
|
+
paths?: unknown;
|
|
40
|
+
Paths?: unknown;
|
|
41
|
+
shell?: unknown;
|
|
42
|
+
Shell?: unknown;
|
|
43
|
+
version?: unknown;
|
|
44
|
+
Version?: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Type guard to check if a value is a plain object
|
|
49
|
+
*/
|
|
50
|
+
function isRecord(value: unknown): value is RawFrontmatter {
|
|
51
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse YAML frontmatter from markdown content
|
|
56
|
+
* Returns the parsed frontmatter and the remaining content
|
|
57
|
+
*/
|
|
58
|
+
export function parseFrontmatter(content: string): {
|
|
59
|
+
frontmatter: RawFrontmatter;
|
|
60
|
+
body: string;
|
|
61
|
+
} {
|
|
62
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
|
|
63
|
+
const match = content.match(frontmatterRegex);
|
|
64
|
+
|
|
65
|
+
if (!match) {
|
|
66
|
+
return { frontmatter: {}, body: content };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const parsedYaml: unknown = yaml.parse(match[1]!);
|
|
71
|
+
const frontmatter: RawFrontmatter = isRecord(parsedYaml) ? parsedYaml : {};
|
|
72
|
+
const body = match[2] ?? "";
|
|
73
|
+
return { frontmatter, body };
|
|
74
|
+
} catch {
|
|
75
|
+
return { frontmatter: {}, body: content };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convert array or string frontmatter value to array
|
|
81
|
+
*/
|
|
82
|
+
export function toArray(value: unknown): string[] {
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
return value.map(String);
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === "string") {
|
|
87
|
+
return value.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse frontmatter to structured fields
|
|
94
|
+
*/
|
|
95
|
+
export function parseFrontmatterFields(
|
|
96
|
+
frontmatter: RawFrontmatter,
|
|
97
|
+
_body: string,
|
|
98
|
+
fileName: string
|
|
99
|
+
): {
|
|
100
|
+
name?: string;
|
|
101
|
+
description?: string | string[];
|
|
102
|
+
when_to_use?: string;
|
|
103
|
+
argument_hint?: string;
|
|
104
|
+
arguments?: string[];
|
|
105
|
+
allowed_tools?: string[];
|
|
106
|
+
model?: string;
|
|
107
|
+
disable_model_invocation?: boolean;
|
|
108
|
+
user_invocable?: boolean;
|
|
109
|
+
hooks?: { "pre-task"?: string[]; "post-task"?: string[] };
|
|
110
|
+
context?: "fork" | "inline";
|
|
111
|
+
agent?: string;
|
|
112
|
+
effort?: string;
|
|
113
|
+
paths?: string[];
|
|
114
|
+
shell?: { name?: string; command?: string; env?: Record<string, string> };
|
|
115
|
+
version?: string;
|
|
116
|
+
} {
|
|
117
|
+
const name =
|
|
118
|
+
(frontmatter.name as string) ??
|
|
119
|
+
fileName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
120
|
+
|
|
121
|
+
const description = frontmatter.description ?? frontmatter.Description;
|
|
122
|
+
const whenToUse = frontmatter.when_to_use ?? frontmatter.whenToUse;
|
|
123
|
+
const argumentHint = frontmatter["argument-hint"] ?? frontmatter.argumentHint ?? frontmatter.ArgumentHint;
|
|
124
|
+
const argumentsList = frontmatter.arguments ?? frontmatter.Arguments;
|
|
125
|
+
const allowedTools = frontmatter["allowed-tools"] ?? frontmatter.allowedTools ?? frontmatter.allowed_tools;
|
|
126
|
+
const model = frontmatter.model ?? frontmatter.Model;
|
|
127
|
+
const disableModelInvocation = frontmatter["disable-model-invocation"] ?? frontmatter.disableModelInvocation ?? false;
|
|
128
|
+
const userInvocable = frontmatter["user-invocable"] ?? frontmatter.userInvocable ?? true;
|
|
129
|
+
const hooks = frontmatter.hooks ?? frontmatter.Hooks;
|
|
130
|
+
const context = frontmatter.context ?? frontmatter.Context;
|
|
131
|
+
const agent = frontmatter.agent ?? frontmatter.Agent;
|
|
132
|
+
const effort = frontmatter.effort ?? frontmatter.Effort;
|
|
133
|
+
const paths = frontmatter.paths ?? frontmatter.Paths;
|
|
134
|
+
const shell = frontmatter.shell ?? frontmatter.Shell;
|
|
135
|
+
const version = frontmatter.version ?? frontmatter.Version;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
name,
|
|
139
|
+
description: description !== undefined ? toArray(description) : undefined,
|
|
140
|
+
when_to_use: whenToUse as string | undefined,
|
|
141
|
+
argument_hint: argumentHint as string | undefined,
|
|
142
|
+
arguments: argumentsList ? toArray(argumentsList) : undefined,
|
|
143
|
+
allowed_tools: allowedTools ? toArray(allowedTools) : undefined,
|
|
144
|
+
model: model as string | undefined,
|
|
145
|
+
disable_model_invocation: disableModelInvocation as boolean,
|
|
146
|
+
user_invocable: userInvocable as boolean,
|
|
147
|
+
hooks: hooks as { "pre-task"?: string[]; "post-task"?: string[] } | undefined,
|
|
148
|
+
context: context as "fork" | "inline" | undefined,
|
|
149
|
+
agent: agent as string | undefined,
|
|
150
|
+
effort: effort as string | undefined,
|
|
151
|
+
paths: paths ? toArray(paths) : undefined,
|
|
152
|
+
shell: shell as { name?: string; command?: string; env?: Record<string, string> } | undefined,
|
|
153
|
+
version: version as string | undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extract argument names from argument hint string
|
|
159
|
+
* e.g., "<file> <target>" -> ["file", "target"]
|
|
160
|
+
*/
|
|
161
|
+
export function extractArgNames(argumentHint?: string): string[] | undefined {
|
|
162
|
+
if (!argumentHint) return undefined;
|
|
163
|
+
|
|
164
|
+
const matches = argumentHint.matchAll(/<([^>]+)>/g);
|
|
165
|
+
const names: string[] = [];
|
|
166
|
+
for (const match of matches) {
|
|
167
|
+
names.push(match[1]!);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return names.length > 0 ? names : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Estimate token count for content (rough approximation)
|
|
175
|
+
*/
|
|
176
|
+
export function estimateTokens(content: string): number {
|
|
177
|
+
// Rough approximation: ~4 characters per token
|
|
178
|
+
return Math.ceil(content.length / 4);
|
|
179
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Module
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for the Skills system.
|
|
5
|
+
* Re-exports all public types and functions.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Load skills from ~/.config/koi/skills, ~/.claude/skills, and .claude/skills
|
|
9
|
+
* - SKILL.md format with YAML frontmatter
|
|
10
|
+
* - Built-in bundled skills
|
|
11
|
+
* - Conditional skills (path-based activation)
|
|
12
|
+
* - Dynamic skill discovery
|
|
13
|
+
* - Argument substitution ({{skill.args}}, <arg>)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Types
|
|
17
|
+
export type {
|
|
18
|
+
SkillCommand,
|
|
19
|
+
SkillWithPath,
|
|
20
|
+
SkillSource,
|
|
21
|
+
SkillLoadedFrom,
|
|
22
|
+
BundledSkillDefinition,
|
|
23
|
+
SkillFrontmatter,
|
|
24
|
+
SkillInvocationResult,
|
|
25
|
+
ToolUseContext,
|
|
26
|
+
HooksSettings,
|
|
27
|
+
FrontmatterShell,
|
|
28
|
+
ParsedFields,
|
|
29
|
+
} from "./types.js";
|
|
30
|
+
|
|
31
|
+
// Core loader functions
|
|
32
|
+
export {
|
|
33
|
+
loadAllSkills,
|
|
34
|
+
getAllSkills,
|
|
35
|
+
getActiveSkills,
|
|
36
|
+
getSkillByName,
|
|
37
|
+
hasSkill,
|
|
38
|
+
getSkillsBySource,
|
|
39
|
+
getSkillsPath,
|
|
40
|
+
onSkillsLoaded,
|
|
41
|
+
discoverSkillDirsForPaths,
|
|
42
|
+
addSkillDirectories,
|
|
43
|
+
activateConditionalSkillsForPaths,
|
|
44
|
+
getDynamicSkills,
|
|
45
|
+
getConditionalSkillCount,
|
|
46
|
+
resetSkillRegistry,
|
|
47
|
+
} from "./loader.js";
|
|
48
|
+
|
|
49
|
+
// Bundled skills
|
|
50
|
+
export {
|
|
51
|
+
registerBundledSkill,
|
|
52
|
+
getBundledSkillDefinitions,
|
|
53
|
+
initBundledSkills,
|
|
54
|
+
} from "./bundled.js";
|
|
55
|
+
|
|
56
|
+
// Skill invocation
|
|
57
|
+
export {
|
|
58
|
+
parseSkillInvocation,
|
|
59
|
+
isSkillInvocation,
|
|
60
|
+
detectSkillInvocation,
|
|
61
|
+
invokeSkill,
|
|
62
|
+
isSkillAvailable,
|
|
63
|
+
getInvokableSkills,
|
|
64
|
+
formatSkillForDisplay,
|
|
65
|
+
getSkillSuggestions,
|
|
66
|
+
getSkillCountBySource,
|
|
67
|
+
hasAnySkills,
|
|
68
|
+
createToolUseContext,
|
|
69
|
+
} from "./invoke.js";
|
|
70
|
+
|
|
71
|
+
// Substitution utilities
|
|
72
|
+
export {
|
|
73
|
+
substituteArguments,
|
|
74
|
+
parseArgumentNames,
|
|
75
|
+
parseNamedArguments,
|
|
76
|
+
} from "./substitution.js";
|
|
77
|
+
|
|
78
|
+
// Components
|
|
79
|
+
export { SkillsMenu, SkillsMenuStandalone } from "./SkillsMenu.js";
|
|
80
|
+
|
|
81
|
+
// Re-export frontmatter utilities for advanced usage
|
|
82
|
+
export {
|
|
83
|
+
parseFrontmatter,
|
|
84
|
+
parseFrontmatterFields,
|
|
85
|
+
extractArgNames,
|
|
86
|
+
estimateTokens,
|
|
87
|
+
} from "./frontmatter.js";
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Invocation
|
|
3
|
+
*
|
|
4
|
+
* Handles skill detection and execution from user input.
|
|
5
|
+
* Supports Claude Code's slash command format: /skill-name <args>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { SkillCommand, ToolUseContext, ContentBlockParam } from "./types.js";
|
|
10
|
+
import {
|
|
11
|
+
getSkillByName,
|
|
12
|
+
hasSkill,
|
|
13
|
+
getActiveSkills,
|
|
14
|
+
getAllSkills,
|
|
15
|
+
} from "./loader.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse skill name and arguments from input
|
|
19
|
+
* Supports:
|
|
20
|
+
* - /skill-name args
|
|
21
|
+
* - /"skill name" args
|
|
22
|
+
* - /skill-name (no args)
|
|
23
|
+
*/
|
|
24
|
+
export function parseSkillInvocation(text: string): { name: string; args: string } | null {
|
|
25
|
+
const trimmed = text.trim();
|
|
26
|
+
|
|
27
|
+
if (!trimmed.startsWith("/")) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Handle quoted skill names: /"skill name" or /'skill name'
|
|
32
|
+
if (trimmed.startsWith('/"')) {
|
|
33
|
+
const endQuote = trimmed.indexOf('"', 2);
|
|
34
|
+
if (endQuote === -1) return null;
|
|
35
|
+
const name = trimmed.slice(2, endQuote);
|
|
36
|
+
const rest = trimmed.slice(endQuote + 1).trim();
|
|
37
|
+
return { name, args: rest };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (trimmed.startsWith("/'")) {
|
|
41
|
+
const endQuote = trimmed.indexOf("'", 2);
|
|
42
|
+
if (endQuote === -1) return null;
|
|
43
|
+
const name = trimmed.slice(2, endQuote);
|
|
44
|
+
const rest = trimmed.slice(endQuote + 1).trim();
|
|
45
|
+
return { name, args: rest };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Standard slash command: /name args
|
|
49
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
50
|
+
if (spaceIdx === -1) {
|
|
51
|
+
return { name: trimmed.slice(1), args: "" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const name = trimmed.slice(1, spaceIdx);
|
|
55
|
+
const args = trimmed.slice(spaceIdx + 1).trim();
|
|
56
|
+
|
|
57
|
+
return { name, args };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect if input is a skill invocation
|
|
62
|
+
*/
|
|
63
|
+
export function isSkillInvocation(text: string): boolean {
|
|
64
|
+
const parsed = parseSkillInvocation(text);
|
|
65
|
+
if (!parsed) return false;
|
|
66
|
+
return hasSkill(parsed.name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detect skill invocation and return the skill with args
|
|
71
|
+
*/
|
|
72
|
+
export function detectSkillInvocation(
|
|
73
|
+
text: string
|
|
74
|
+
): { skill: SkillCommand; args: string } | null {
|
|
75
|
+
const parsed = parseSkillInvocation(text);
|
|
76
|
+
if (!parsed) return null;
|
|
77
|
+
|
|
78
|
+
// Try exact match first
|
|
79
|
+
let skill = getSkillByName(parsed.name);
|
|
80
|
+
|
|
81
|
+
// If not found, try case-insensitive match
|
|
82
|
+
if (!skill) {
|
|
83
|
+
skill = getSkillByName(parsed.name.toLowerCase());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Try matching with aliases (for bundled skills)
|
|
87
|
+
if (!skill) {
|
|
88
|
+
const allSkills = getAllSkills();
|
|
89
|
+
const nameLower = parsed.name.toLowerCase();
|
|
90
|
+
|
|
91
|
+
for (const s of allSkills) {
|
|
92
|
+
if (s.name.toLowerCase() === nameLower) {
|
|
93
|
+
skill = s;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
// Check aliases if available
|
|
97
|
+
const aliases = (s as { aliases?: string[] }).aliases;
|
|
98
|
+
if (aliases?.some((a) => a.toLowerCase() === nameLower)) {
|
|
99
|
+
skill = s;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!skill) return null;
|
|
106
|
+
|
|
107
|
+
return { skill, args: parsed.args };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a tool use context for skill execution
|
|
112
|
+
*/
|
|
113
|
+
export function createToolUseContext(_session: AgentSession | null): ToolUseContext {
|
|
114
|
+
return {
|
|
115
|
+
tools: {},
|
|
116
|
+
env: process.env as Record<string, unknown>,
|
|
117
|
+
cwd: process.cwd(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Invoke a skill and get the generated prompt content
|
|
123
|
+
*/
|
|
124
|
+
export async function invokeSkill(
|
|
125
|
+
skill: SkillCommand,
|
|
126
|
+
args: string,
|
|
127
|
+
session: AgentSession | null
|
|
128
|
+
): Promise<ContentBlockParam[]> {
|
|
129
|
+
const ctx = createToolUseContext(session);
|
|
130
|
+
return skill.getPromptForCommand(args, ctx);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a skill is available for invocation
|
|
135
|
+
*/
|
|
136
|
+
export function isSkillAvailable(name: string): boolean {
|
|
137
|
+
return hasSkill(name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get all invokable skills (user invocable ones)
|
|
142
|
+
*/
|
|
143
|
+
export function getInvokableSkills(): SkillCommand[] {
|
|
144
|
+
return getActiveSkills().filter((skill) => skill.userInvocable);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Format skill for display
|
|
149
|
+
*/
|
|
150
|
+
export function formatSkillForDisplay(skill: SkillCommand): {
|
|
151
|
+
name: string;
|
|
152
|
+
description: string;
|
|
153
|
+
usage: string;
|
|
154
|
+
} {
|
|
155
|
+
const usage = skill.argumentHint
|
|
156
|
+
? `/${skill.name} ${skill.argumentHint}`
|
|
157
|
+
: `/${skill.name}`;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
name: skill.name,
|
|
161
|
+
description: skill.description,
|
|
162
|
+
usage,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if input might be a skill and return suggested skill names
|
|
168
|
+
*/
|
|
169
|
+
export function getSkillSuggestions(input: string, limit = 5): string[] {
|
|
170
|
+
const trimmed = input.trim();
|
|
171
|
+
if (!trimmed.startsWith("/")) return [];
|
|
172
|
+
|
|
173
|
+
const query = trimmed.slice(1).toLowerCase();
|
|
174
|
+
if (!query) return [];
|
|
175
|
+
|
|
176
|
+
const allSkills = getActiveSkills();
|
|
177
|
+
const suggestions: Array<{ skill: SkillCommand; score: number }> = [];
|
|
178
|
+
|
|
179
|
+
for (const skill of allSkills) {
|
|
180
|
+
if (!skill.userInvocable) continue;
|
|
181
|
+
|
|
182
|
+
const nameLower = skill.name.toLowerCase();
|
|
183
|
+
let score = 0;
|
|
184
|
+
|
|
185
|
+
// Exact match
|
|
186
|
+
if (nameLower === query) {
|
|
187
|
+
score = 100;
|
|
188
|
+
}
|
|
189
|
+
// Starts with
|
|
190
|
+
else if (nameLower.startsWith(query)) {
|
|
191
|
+
score = 50;
|
|
192
|
+
}
|
|
193
|
+
// Contains
|
|
194
|
+
else if (nameLower.includes(query)) {
|
|
195
|
+
score = 25;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (score > 0) {
|
|
199
|
+
suggestions.push({ skill, score });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Sort by score descending, then alphabetically
|
|
204
|
+
suggestions.sort((a, b) => {
|
|
205
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
206
|
+
return a.skill.name.localeCompare(b.skill.name);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return suggestions.slice(0, limit).map((s) => s.skill.name);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get skill count by source
|
|
214
|
+
*/
|
|
215
|
+
export function getSkillCountBySource(): Record<string, number> {
|
|
216
|
+
const bySource = new Map<string, number>();
|
|
217
|
+
|
|
218
|
+
for (const skill of getActiveSkills()) {
|
|
219
|
+
const count = bySource.get(skill.source) ?? 0;
|
|
220
|
+
bySource.set(skill.source, count + 1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return Object.fromEntries(bySource);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if there are any skills available
|
|
228
|
+
*/
|
|
229
|
+
export function hasAnySkills(): boolean {
|
|
230
|
+
return getActiveSkills().length > 0;
|
|
231
|
+
}
|