@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.
Files changed (109) hide show
  1. package/LICENSE +34 -0
  2. package/NOTICE +35 -0
  3. package/README.md +15 -0
  4. package/bin/koi +12 -0
  5. package/dist/highlights-eq9cgrbb.scm +604 -0
  6. package/dist/highlights-ghv9g403.scm +205 -0
  7. package/dist/highlights-hk7bwhj4.scm +284 -0
  8. package/dist/highlights-r812a2qc.scm +150 -0
  9. package/dist/highlights-x6tmsnaa.scm +115 -0
  10. package/dist/injections-73j83es3.scm +27 -0
  11. package/dist/main.js +489918 -0
  12. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  13. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  14. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  15. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  16. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  17. package/package.json +51 -0
  18. package/src/agent/check-permissions.ts +239 -0
  19. package/src/agent/hooks/message-utils.ts +305 -0
  20. package/src/agent/hooks/types.ts +32 -0
  21. package/src/agent/hooks.ts +1560 -0
  22. package/src/agent/mode.ts +163 -0
  23. package/src/agent/monitor-registry.ts +308 -0
  24. package/src/agent/permission-ui.ts +71 -0
  25. package/src/agent/plan-ui.ts +74 -0
  26. package/src/agent/question-ui.ts +58 -0
  27. package/src/agent/session-fork.ts +299 -0
  28. package/src/agent/session-snapshots.ts +216 -0
  29. package/src/agent/session-store.ts +649 -0
  30. package/src/agent/session-tasks.ts +305 -0
  31. package/src/agent/session.ts +27 -0
  32. package/src/agent/subagent-registry.ts +176 -0
  33. package/src/agent/subagent.ts +194 -0
  34. package/src/agent/tool-orchestration.ts +55 -0
  35. package/src/agent/tools.ts +8 -0
  36. package/src/cli/args.ts +6 -0
  37. package/src/cli/commands.ts +5 -0
  38. package/src/commands/skills/index.ts +23 -0
  39. package/src/config/models.ts +6 -0
  40. package/src/config/settings.ts +392 -0
  41. package/src/main.tsx +64 -0
  42. package/src/services/mcp/client.ts +194 -0
  43. package/src/services/mcp/config.ts +232 -0
  44. package/src/services/mcp/connection-manager.ts +258 -0
  45. package/src/services/mcp/index.ts +80 -0
  46. package/src/services/mcp/mcp-commands.ts +114 -0
  47. package/src/services/mcp/stdio-transport.ts +246 -0
  48. package/src/services/mcp/types.ts +155 -0
  49. package/src/skills/SkillsMenu.tsx +370 -0
  50. package/src/skills/bundled/batch.ts +106 -0
  51. package/src/skills/bundled/debug.ts +86 -0
  52. package/src/skills/bundled/loremIpsum.ts +101 -0
  53. package/src/skills/bundled/remember.ts +97 -0
  54. package/src/skills/bundled/simplify.ts +100 -0
  55. package/src/skills/bundled/skillify.ts +123 -0
  56. package/src/skills/bundled/stuck.ts +101 -0
  57. package/src/skills/bundled/updateConfig.ts +228 -0
  58. package/src/skills/bundled.ts +46 -0
  59. package/src/skills/frontmatter.ts +179 -0
  60. package/src/skills/index.ts +87 -0
  61. package/src/skills/invoke.ts +231 -0
  62. package/src/skills/loader.ts +710 -0
  63. package/src/skills/substitution.ts +169 -0
  64. package/src/skills/types.ts +201 -0
  65. package/src/tools/agent.ts +143 -0
  66. package/src/tools/ask-user-question.ts +46 -0
  67. package/src/tools/bash.ts +148 -0
  68. package/src/tools/edit.ts +164 -0
  69. package/src/tools/glob.ts +102 -0
  70. package/src/tools/grep.ts +248 -0
  71. package/src/tools/index.ts +73 -0
  72. package/src/tools/list-mcp-resources.ts +74 -0
  73. package/src/tools/ls.ts +85 -0
  74. package/src/tools/mcp.ts +76 -0
  75. package/src/tools/monitor.ts +159 -0
  76. package/src/tools/plan-mode.ts +134 -0
  77. package/src/tools/read-mcp-resource.ts +79 -0
  78. package/src/tools/read.ts +137 -0
  79. package/src/tools/skill.ts +176 -0
  80. package/src/tools/task.ts +349 -0
  81. package/src/tools/types.ts +52 -0
  82. package/src/tools/webfetch-domains.ts +239 -0
  83. package/src/tools/webfetch.ts +533 -0
  84. package/src/tools/write.ts +101 -0
  85. package/src/tui/app.tsx +1178 -0
  86. package/src/tui/components/chat-panel.tsx +1071 -0
  87. package/src/tui/components/command-panel.tsx +261 -0
  88. package/src/tui/components/confirm-modal.tsx +135 -0
  89. package/src/tui/components/connect-modal.tsx +435 -0
  90. package/src/tui/components/connecting-modal.tsx +167 -0
  91. package/src/tui/components/edit-pending-modal.tsx +103 -0
  92. package/src/tui/components/exit-modal.tsx +131 -0
  93. package/src/tui/components/fork-modal.tsx +377 -0
  94. package/src/tui/components/image-preview-modal.tsx +141 -0
  95. package/src/tui/components/image-utils.ts +128 -0
  96. package/src/tui/components/info-bar.tsx +103 -0
  97. package/src/tui/components/input-box.tsx +352 -0
  98. package/src/tui/components/mcp/MCPSettings.tsx +386 -0
  99. package/src/tui/components/mcp/index.ts +7 -0
  100. package/src/tui/components/model-modal.tsx +310 -0
  101. package/src/tui/components/pending-area.tsx +88 -0
  102. package/src/tui/components/rename-modal.tsx +119 -0
  103. package/src/tui/components/session-modal.tsx +233 -0
  104. package/src/tui/components/side-bar.tsx +349 -0
  105. package/src/tui/components/tool-output.ts +6 -0
  106. package/src/tui/hooks/user-prompt-history.ts +114 -0
  107. package/src/tui/theme.ts +63 -0
  108. package/src/types/commands.ts +80 -0
  109. 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
+ }