@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,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillsMenu Component
|
|
3
|
+
*
|
|
4
|
+
* A modal dialog that displays available skills grouped by source,
|
|
5
|
+
* with search filtering and detailed skill information.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
10
|
+
import { createTextAttributes } from "@opentui/core";
|
|
11
|
+
import type { TextareaRenderable } from "@opentui/core";
|
|
12
|
+
import type { SkillCommand, SkillSource } from "./types.js";
|
|
13
|
+
|
|
14
|
+
interface SkillsMenuProps {
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
skills: SkillCommand[];
|
|
18
|
+
onInvokeSkill?: (skill: SkillCommand, args: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SkillGroup {
|
|
22
|
+
source: SkillSource;
|
|
23
|
+
label: string;
|
|
24
|
+
skills: SkillCommand[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SOURCE_LABELS: Record<SkillSource, string> = {
|
|
28
|
+
userSettings: "User Skills",
|
|
29
|
+
projectSettings: "Project Skills",
|
|
30
|
+
policySettings: "Policy Skills",
|
|
31
|
+
plugin: "Plugin Skills",
|
|
32
|
+
bundled: "Built-in Skills",
|
|
33
|
+
mcp: "MCP Skills",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* SkillsMenu component
|
|
38
|
+
*/
|
|
39
|
+
export function SkillsMenu({ isActive, onClose, skills, onInvokeSkill }: SkillsMenuProps) {
|
|
40
|
+
const { width, height } = useTerminalDimensions();
|
|
41
|
+
const inputRef = useRef<TextareaRenderable>(null);
|
|
42
|
+
const [filterText, setFilterText] = useState("");
|
|
43
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
44
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
45
|
+
const scrollOffsetRef = useRef(0);
|
|
46
|
+
|
|
47
|
+
const panelWidth = Math.min(80, Math.max(50, Math.floor(width * 0.8)));
|
|
48
|
+
const panelHeight = Math.min(height - 4, 25);
|
|
49
|
+
const inputHeight = 1;
|
|
50
|
+
const separatorHeight = 1;
|
|
51
|
+
const detailsHeight = showDetails ? 8 : 0;
|
|
52
|
+
const listHeight = panelHeight - inputHeight - separatorHeight - detailsHeight - 2;
|
|
53
|
+
|
|
54
|
+
// Group skills by source
|
|
55
|
+
const skillGroups = useMemo<SkillGroup[]>(() => {
|
|
56
|
+
const groups: Map<SkillSource, SkillCommand[]> = new Map();
|
|
57
|
+
|
|
58
|
+
for (const skill of skills) {
|
|
59
|
+
const existing = groups.get(skill.source) ?? [];
|
|
60
|
+
existing.push(skill);
|
|
61
|
+
groups.set(skill.source, existing);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Array.from(groups.entries())
|
|
65
|
+
.filter(([_, groupSkills]) => groupSkills.length > 0)
|
|
66
|
+
.map(([source, groupSkills]) => ({
|
|
67
|
+
source,
|
|
68
|
+
label: SOURCE_LABELS[source] ?? source,
|
|
69
|
+
skills: groupSkills,
|
|
70
|
+
}));
|
|
71
|
+
}, [skills]);
|
|
72
|
+
|
|
73
|
+
// Flatten skills with filtering
|
|
74
|
+
const { flatItems, totalSkills } = useMemo(() => {
|
|
75
|
+
const query = filterText.toLowerCase().trim();
|
|
76
|
+
|
|
77
|
+
const filteredGroups = skillGroups
|
|
78
|
+
.map((group) => ({
|
|
79
|
+
...group,
|
|
80
|
+
skills: group.skills.filter(
|
|
81
|
+
(skill) =>
|
|
82
|
+
!query ||
|
|
83
|
+
skill.name.toLowerCase().includes(query) ||
|
|
84
|
+
skill.description.toLowerCase().includes(query) ||
|
|
85
|
+
skill.whenToUse?.toLowerCase().includes(query)
|
|
86
|
+
),
|
|
87
|
+
}))
|
|
88
|
+
.filter((group) => group.skills.length > 0);
|
|
89
|
+
|
|
90
|
+
const items: Array<{ type: "header" | "skill"; group?: SkillGroup; skill?: SkillCommand }> = [];
|
|
91
|
+
for (const group of filteredGroups) {
|
|
92
|
+
items.push({ type: "header", group });
|
|
93
|
+
for (const skill of group.skills) {
|
|
94
|
+
items.push({ type: "skill", group, skill });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const total = skills.length;
|
|
99
|
+
return { flatItems: items, totalSkills: total };
|
|
100
|
+
}, [skillGroups, filterText, skills.length]);
|
|
101
|
+
|
|
102
|
+
// Reset when opened
|
|
103
|
+
useLayoutEffect(() => {
|
|
104
|
+
if (isActive) {
|
|
105
|
+
setFilterText("");
|
|
106
|
+
setSelectedIndex(0);
|
|
107
|
+
setShowDetails(false);
|
|
108
|
+
scrollOffsetRef.current = 0;
|
|
109
|
+
const ta = inputRef.current;
|
|
110
|
+
if (ta) {
|
|
111
|
+
ta.editBuffer.replaceText("");
|
|
112
|
+
ta.focus();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}, [isActive]);
|
|
116
|
+
|
|
117
|
+
// Get selected skill
|
|
118
|
+
const selectedItem = flatItems[selectedIndex];
|
|
119
|
+
const selectedSkill = selectedItem?.type === "skill" ? selectedItem.skill : null;
|
|
120
|
+
|
|
121
|
+
// Keyboard handling
|
|
122
|
+
useKeyboard((key) => {
|
|
123
|
+
if (!isActive) return;
|
|
124
|
+
|
|
125
|
+
if (key.ctrl && key.name === "p") {
|
|
126
|
+
key.preventDefault();
|
|
127
|
+
key.stopPropagation();
|
|
128
|
+
onClose();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (key.name === "escape") {
|
|
133
|
+
key.preventDefault();
|
|
134
|
+
key.stopPropagation();
|
|
135
|
+
onClose();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (key.name === "up" || key.name === "down") {
|
|
140
|
+
key.preventDefault();
|
|
141
|
+
key.stopPropagation();
|
|
142
|
+
|
|
143
|
+
const newIndex = key.name === "up"
|
|
144
|
+
? Math.max(0, selectedIndex - 1)
|
|
145
|
+
: Math.min(flatItems.length - 1, selectedIndex + 1);
|
|
146
|
+
|
|
147
|
+
const newItem = flatItems[newIndex];
|
|
148
|
+
// Skip headers when navigating
|
|
149
|
+
if (newItem?.type === "header") {
|
|
150
|
+
setSelectedIndex(key.name === "up" ? Math.max(0, newIndex - 1) : Math.min(flatItems.length - 1, newIndex + 1));
|
|
151
|
+
} else {
|
|
152
|
+
setSelectedIndex(newIndex);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Scroll handling
|
|
156
|
+
const currentScroll = scrollOffsetRef.current;
|
|
157
|
+
const newFlatIndex = flatItems.findIndex((item, idx) => idx === newIndex && item.type === "skill");
|
|
158
|
+
if (newFlatIndex !== -1) {
|
|
159
|
+
if (newFlatIndex < currentScroll) {
|
|
160
|
+
scrollOffsetRef.current = newFlatIndex;
|
|
161
|
+
} else if (newFlatIndex > currentScroll + listHeight - 1) {
|
|
162
|
+
scrollOffsetRef.current = newFlatIndex - listHeight + 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (key.name === "return" || key.name === "enter") {
|
|
169
|
+
key.preventDefault();
|
|
170
|
+
key.stopPropagation();
|
|
171
|
+
|
|
172
|
+
if (selectedSkill) {
|
|
173
|
+
if (onInvokeSkill && selectedSkill.argumentHint) {
|
|
174
|
+
// Show details and allow typing arguments
|
|
175
|
+
setShowDetails(true);
|
|
176
|
+
} else if (onInvokeSkill) {
|
|
177
|
+
onInvokeSkill(selectedSkill, "");
|
|
178
|
+
onClose();
|
|
179
|
+
} else {
|
|
180
|
+
// Just close and show the skill was selected
|
|
181
|
+
onClose();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (key.name === "tab") {
|
|
188
|
+
key.preventDefault();
|
|
189
|
+
key.stopPropagation();
|
|
190
|
+
setShowDetails((prev) => !prev);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (key.name === "right") {
|
|
195
|
+
key.preventDefault();
|
|
196
|
+
key.stopPropagation();
|
|
197
|
+
if (selectedSkill) {
|
|
198
|
+
setShowDetails(true);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const handleContentChange = () => {
|
|
205
|
+
const text = inputRef.current?.editBuffer.getText() ?? "";
|
|
206
|
+
setFilterText(text);
|
|
207
|
+
setSelectedIndex(0);
|
|
208
|
+
scrollOffsetRef.current = 0;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (!isActive) return null;
|
|
212
|
+
|
|
213
|
+
const effectiveScrollOffset = scrollOffsetRef.current;
|
|
214
|
+
const visibleItems = flatItems.slice(effectiveScrollOffset, effectiveScrollOffset + listHeight);
|
|
215
|
+
|
|
216
|
+
// Render details panel
|
|
217
|
+
const renderDetails = () => {
|
|
218
|
+
if (!showDetails || !selectedSkill) return null;
|
|
219
|
+
|
|
220
|
+
const details = [
|
|
221
|
+
selectedSkill.description && `Description: ${selectedSkill.description}`,
|
|
222
|
+
selectedSkill.whenToUse && `When to use: ${selectedSkill.whenToUse}`,
|
|
223
|
+
selectedSkill.argumentHint && `Arguments: ${selectedSkill.argumentHint}`,
|
|
224
|
+
selectedSkill.allowedTools.length > 0 && `Allowed tools: ${selectedSkill.allowedTools.join(", ")}`,
|
|
225
|
+
selectedSkill.context && `Context: ${selectedSkill.context}`,
|
|
226
|
+
].filter(Boolean);
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<box height={detailsHeight} flexDirection="column" marginTop={1}>
|
|
230
|
+
<box height={1}>
|
|
231
|
+
<text fg="#6272a4">
|
|
232
|
+
{"─".repeat(panelWidth - 4)}
|
|
233
|
+
</text>
|
|
234
|
+
</box>
|
|
235
|
+
{details.map((line, idx) => (
|
|
236
|
+
<box key={idx} height={1}>
|
|
237
|
+
<text fg="#8be9fd" wrapMode="none">{line}</text>
|
|
238
|
+
</box>
|
|
239
|
+
))}
|
|
240
|
+
</box>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<box
|
|
246
|
+
position="absolute"
|
|
247
|
+
top={0}
|
|
248
|
+
left={0}
|
|
249
|
+
width="100%"
|
|
250
|
+
height="100%"
|
|
251
|
+
backgroundColor="#00000080"
|
|
252
|
+
alignItems="center"
|
|
253
|
+
justifyContent="center"
|
|
254
|
+
>
|
|
255
|
+
<box
|
|
256
|
+
width={panelWidth}
|
|
257
|
+
height={panelHeight}
|
|
258
|
+
flexDirection="column"
|
|
259
|
+
borderStyle="rounded"
|
|
260
|
+
borderColor="#6272a4"
|
|
261
|
+
backgroundColor="#282a36"
|
|
262
|
+
paddingX={1}
|
|
263
|
+
paddingY={1}
|
|
264
|
+
>
|
|
265
|
+
{/* Header */}
|
|
266
|
+
<box height={1} flexDirection="row" justifyContent="space-between">
|
|
267
|
+
<text fg="#ff79c6" attributes={createTextAttributes({ bold: true })}>
|
|
268
|
+
Skills ({totalSkills})
|
|
269
|
+
</text>
|
|
270
|
+
<text fg="#6272a4">
|
|
271
|
+
↑↓ navigate · Enter select · Tab details · Esc close
|
|
272
|
+
</text>
|
|
273
|
+
</box>
|
|
274
|
+
|
|
275
|
+
{/* Filter input */}
|
|
276
|
+
<box height={inputHeight} marginTop={1}>
|
|
277
|
+
<text fg="#6272a4">Filter: </text>
|
|
278
|
+
<textarea
|
|
279
|
+
ref={inputRef}
|
|
280
|
+
initialValue=""
|
|
281
|
+
focused={isActive}
|
|
282
|
+
showCursor
|
|
283
|
+
height={1}
|
|
284
|
+
wrapMode="none"
|
|
285
|
+
width={panelWidth - 10}
|
|
286
|
+
textColor="#f8f8f2"
|
|
287
|
+
backgroundColor="#44475a"
|
|
288
|
+
onContentChange={handleContentChange}
|
|
289
|
+
/>
|
|
290
|
+
</box>
|
|
291
|
+
|
|
292
|
+
{/* Separator */}
|
|
293
|
+
<box height={separatorHeight} marginTop={1}>
|
|
294
|
+
<text fg="#6272a4">
|
|
295
|
+
{"─".repeat(panelWidth - 2)}
|
|
296
|
+
</text>
|
|
297
|
+
</box>
|
|
298
|
+
|
|
299
|
+
{/* Skill list */}
|
|
300
|
+
<box height={listHeight} flexDirection="column" overflow="hidden">
|
|
301
|
+
{visibleItems.map((item, idx) => {
|
|
302
|
+
const flatIndex = effectiveScrollOffset + idx;
|
|
303
|
+
|
|
304
|
+
if (item.type === "header") {
|
|
305
|
+
return (
|
|
306
|
+
<box key={`h-${item.group?.source}-${flatIndex}`} height={1}>
|
|
307
|
+
<text
|
|
308
|
+
fg="#bd93f9"
|
|
309
|
+
attributes={createTextAttributes({ bold: true })}
|
|
310
|
+
>
|
|
311
|
+
{item.group?.label}
|
|
312
|
+
</text>
|
|
313
|
+
</box>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (item.type === "skill" && item.skill) {
|
|
318
|
+
const isSelected = flatIndex === selectedIndex;
|
|
319
|
+
const safeDescription = (item.skill.description ?? "").slice(0, 50);
|
|
320
|
+
return (
|
|
321
|
+
<box
|
|
322
|
+
key={`s-${item.skill.name}-${flatIndex}`}
|
|
323
|
+
height={1}
|
|
324
|
+
backgroundColor={isSelected ? "#44475a" : undefined}
|
|
325
|
+
paddingLeft={2}
|
|
326
|
+
flexDirection="row"
|
|
327
|
+
>
|
|
328
|
+
<text fg="#ffb86c">/{item.skill.name}</text>
|
|
329
|
+
<text fg="#6272a4"> {safeDescription}{item.skill.description && item.skill.description.length > 50 ? "..." : ""}</text>
|
|
330
|
+
</box>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return null;
|
|
335
|
+
})}
|
|
336
|
+
{flatItems.length === 0 && (
|
|
337
|
+
<box height={listHeight} alignItems="center" justifyContent="center">
|
|
338
|
+
<text fg="#6272a4">No skills found</text>
|
|
339
|
+
</box>
|
|
340
|
+
)}
|
|
341
|
+
</box>
|
|
342
|
+
|
|
343
|
+
{/* Details panel */}
|
|
344
|
+
{renderDetails()}
|
|
345
|
+
</box>
|
|
346
|
+
</box>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* SkillsMenuStandalone - A standalone modal version that doesn't need isActive prop
|
|
352
|
+
*/
|
|
353
|
+
export function SkillsMenuStandalone({
|
|
354
|
+
skills,
|
|
355
|
+
onClose,
|
|
356
|
+
onInvokeSkill,
|
|
357
|
+
}: {
|
|
358
|
+
skills: SkillCommand[];
|
|
359
|
+
onClose: () => void;
|
|
360
|
+
onInvokeSkill?: (skill: SkillCommand, args: string) => void;
|
|
361
|
+
}) {
|
|
362
|
+
return (
|
|
363
|
+
<SkillsMenu
|
|
364
|
+
isActive={true}
|
|
365
|
+
onClose={onClose}
|
|
366
|
+
skills={skills}
|
|
367
|
+
onInvokeSkill={onInvokeSkill}
|
|
368
|
+
/>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch Skill
|
|
3
|
+
*
|
|
4
|
+
* Orchestrate large-scale changes across multiple parallel agents.
|
|
5
|
+
* Adapted from Claude Code's batch skill.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { registerBundledSkill } from "../bundled.js";
|
|
9
|
+
|
|
10
|
+
const MIN_AGENTS = 3;
|
|
11
|
+
const MAX_AGENTS = 20;
|
|
12
|
+
|
|
13
|
+
const WORKER_INSTRUCTIONS = `After implementing the change:
|
|
14
|
+
1. **Review** — Use /simplify to review and clean up changes
|
|
15
|
+
2. **Test** — Run the project's test suite
|
|
16
|
+
3. **Commit** — Commit changes with a clear message
|
|
17
|
+
4. **Report** — End with a summary of what was done`;
|
|
18
|
+
|
|
19
|
+
const NOT_A_GIT_REPO_MESSAGE = `This is not a git repository. The \`/batch\` command requires a git repo. Initialize a repo first, or run this from inside an existing one.`;
|
|
20
|
+
|
|
21
|
+
const MISSING_INSTRUCTION_MESSAGE = `Provide an instruction describing the batch change you want to make.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
/batch add TypeScript to all JavaScript files
|
|
25
|
+
/batch replace lodash with native equivalents
|
|
26
|
+
/batch add error handling to all API routes`;
|
|
27
|
+
|
|
28
|
+
export function registerBatchSkill(): void {
|
|
29
|
+
registerBundledSkill({
|
|
30
|
+
name: "batch",
|
|
31
|
+
description:
|
|
32
|
+
"Plan and execute a large-scale change across multiple parallel agents.",
|
|
33
|
+
whenToUse:
|
|
34
|
+
"Use when making sweeping changes across many files that can be decomposed into independent parallel units.",
|
|
35
|
+
argumentHint: "<instruction>",
|
|
36
|
+
userInvocable: true,
|
|
37
|
+
disableModelInvocation: true,
|
|
38
|
+
async getPromptForCommand(args) {
|
|
39
|
+
const instruction = args.trim();
|
|
40
|
+
if (!instruction) {
|
|
41
|
+
return [{ type: "text", text: MISSING_INSTRUCTION_MESSAGE }];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const prompt = `# Batch: Parallel Work Orchestration
|
|
45
|
+
|
|
46
|
+
You are orchestrating a large, parallelizable change across this codebase.
|
|
47
|
+
|
|
48
|
+
## User Instruction
|
|
49
|
+
|
|
50
|
+
${instruction}
|
|
51
|
+
|
|
52
|
+
## Phase 1: Research and Plan
|
|
53
|
+
|
|
54
|
+
1. **Understand the scope.** Research what files, patterns, and locations need to change.
|
|
55
|
+
2. **Decompose into units.** Break the work into ${MIN_AGENTS}–${MAX_AGENTS} self-contained units. Each unit must:
|
|
56
|
+
- Be independently implementable
|
|
57
|
+
- Be mergeable without depending on another unit
|
|
58
|
+
- Be roughly uniform in size
|
|
59
|
+
|
|
60
|
+
Scale the count to the actual work: few files → closer to ${MIN_AGENTS}; many files → closer to ${MAX_AGENTS}.
|
|
61
|
+
|
|
62
|
+
3. **Determine verification.** Figure out how to verify each change works:
|
|
63
|
+
- Run tests
|
|
64
|
+
- Check for linting errors
|
|
65
|
+
- Verify the change visually if applicable
|
|
66
|
+
|
|
67
|
+
4. **Write the plan.** Include:
|
|
68
|
+
- Summary of scope
|
|
69
|
+
- Numbered list of work units with file lists
|
|
70
|
+
- Verification approach for each unit
|
|
71
|
+
|
|
72
|
+
## Phase 2: Execute in Parallel
|
|
73
|
+
|
|
74
|
+
Spawn one agent per work unit. Each agent should:
|
|
75
|
+
- Have the full context of what to do
|
|
76
|
+
- Know the files to modify
|
|
77
|
+
- Understand the verification approach
|
|
78
|
+
- Follow these instructions:
|
|
79
|
+
|
|
80
|
+
\`\`\`
|
|
81
|
+
${WORKER_INSTRUCTIONS}
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
## Phase 3: Track Progress
|
|
85
|
+
|
|
86
|
+
Track progress with a status table:
|
|
87
|
+
|
|
88
|
+
| # | Unit | Status |
|
|
89
|
+
|---|------|--------|
|
|
90
|
+
| 1 | <title> | running |
|
|
91
|
+
| 2 | <title> | done |
|
|
92
|
+
|
|
93
|
+
As agents complete, update the status and summarize results.
|
|
94
|
+
|
|
95
|
+
## Final Summary
|
|
96
|
+
|
|
97
|
+
When all agents finish, provide a summary of:
|
|
98
|
+
- How many units completed successfully
|
|
99
|
+
- Any failures or issues encountered
|
|
100
|
+
- Overall impact of the changes
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
return [{ type: "text", text: prompt }];
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Skill
|
|
3
|
+
*
|
|
4
|
+
* Enable debug logging and help diagnose issues in Koi.
|
|
5
|
+
* Adapted from Claude Code's debug skill.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { registerBundledSkill } from "../bundled.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_DEBUG_LINES_READ = 30;
|
|
11
|
+
|
|
12
|
+
export function registerDebugSkill(): void {
|
|
13
|
+
registerBundledSkill({
|
|
14
|
+
name: "debug",
|
|
15
|
+
description:
|
|
16
|
+
"Enable debug logging for this session and help diagnose issues. Read debug logs and provide diagnostic information.",
|
|
17
|
+
allowedTools: ["Read", "Grep", "Glob"],
|
|
18
|
+
argumentHint: "[issue description]",
|
|
19
|
+
disableModelInvocation: true,
|
|
20
|
+
userInvocable: true,
|
|
21
|
+
async getPromptForCommand(args) {
|
|
22
|
+
const prompt = `# Debug Skill
|
|
23
|
+
|
|
24
|
+
Help the user debug an issue they're encountering in this Koi session.
|
|
25
|
+
|
|
26
|
+
## Session Context
|
|
27
|
+
|
|
28
|
+
Working directory: \`{{cwd}}\`
|
|
29
|
+
Current time: {{timestamp}}
|
|
30
|
+
|
|
31
|
+
## Issue Description
|
|
32
|
+
|
|
33
|
+
${args || "The user did not describe a specific issue. Ask clarifying questions to understand what problem they are experiencing."}
|
|
34
|
+
|
|
35
|
+
## Diagnostic Steps
|
|
36
|
+
|
|
37
|
+
### 1. Gather Information
|
|
38
|
+
Ask the user to describe:
|
|
39
|
+
- What were you trying to do?
|
|
40
|
+
- What happened instead?
|
|
41
|
+
- When did this start happening?
|
|
42
|
+
- What changed recently?
|
|
43
|
+
|
|
44
|
+
### 2. Check Common Issues
|
|
45
|
+
|
|
46
|
+
**Connection Issues:**
|
|
47
|
+
- Is the API provider configured correctly?
|
|
48
|
+
- Is the API key valid?
|
|
49
|
+
- Is there a network connectivity issue?
|
|
50
|
+
|
|
51
|
+
**Model Issues:**
|
|
52
|
+
- Is the model ID correct?
|
|
53
|
+
- Is the model available for this provider?
|
|
54
|
+
- Are there rate limit errors?
|
|
55
|
+
|
|
56
|
+
**Tool Issues:**
|
|
57
|
+
- Are the required tools available?
|
|
58
|
+
- Is there a permission issue?
|
|
59
|
+
- Are there file system errors?
|
|
60
|
+
|
|
61
|
+
### 3. Suggest Next Steps
|
|
62
|
+
|
|
63
|
+
Based on the issue, suggest:
|
|
64
|
+
- Configuration fixes
|
|
65
|
+
- Alternative approaches
|
|
66
|
+
- Commands to run for more diagnostics
|
|
67
|
+
- Settings to check
|
|
68
|
+
|
|
69
|
+
## Settings Files to Check
|
|
70
|
+
|
|
71
|
+
- Koi Settings: \`~/.config/koi/settings.json\`
|
|
72
|
+
- Pi Settings: \`~/.config/koi/pi/settings.json\`
|
|
73
|
+
|
|
74
|
+
## Instructions
|
|
75
|
+
|
|
76
|
+
1. Ask clarifying questions if the issue is unclear
|
|
77
|
+
2. Read relevant configuration files
|
|
78
|
+
3. Identify the root cause
|
|
79
|
+
4. Provide specific, actionable fixes
|
|
80
|
+
5. Explain what went wrong in plain language
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
return [{ type: "text", text: prompt }];
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lorem Ipsum Skill
|
|
3
|
+
*
|
|
4
|
+
* Generate filler text for testing long context handling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { registerBundledSkill } from "../bundled.js";
|
|
8
|
+
|
|
9
|
+
// Verified 1-token words (common English words)
|
|
10
|
+
const ONE_TOKEN_WORDS = [
|
|
11
|
+
"the", "a", "an", "I", "you", "he", "she", "it", "we", "they",
|
|
12
|
+
"me", "him", "her", "us", "them", "my", "your", "his", "its", "our",
|
|
13
|
+
"this", "that", "what", "who", "is", "are", "was", "were", "be",
|
|
14
|
+
"been", "have", "has", "had", "do", "does", "did", "will", "would",
|
|
15
|
+
"can", "could", "may", "might", "must", "shall", "should", "make",
|
|
16
|
+
"made", "get", "got", "go", "went", "come", "came", "see", "saw",
|
|
17
|
+
"know", "take", "think", "look", "want", "use", "find", "give",
|
|
18
|
+
"tell", "work", "call", "try", "ask", "need", "feel", "seem",
|
|
19
|
+
"leave", "put", "time", "year", "day", "way", "man", "thing",
|
|
20
|
+
"life", "hand", "part", "place", "case", "point", "fact", "good",
|
|
21
|
+
"new", "first", "last", "long", "great", "little", "own", "other",
|
|
22
|
+
"old", "right", "big", "high", "small", "large", "next", "early",
|
|
23
|
+
"young", "few", "public", "bad", "same", "able", "in", "on", "at",
|
|
24
|
+
"to", "for", "of", "with", "from", "by", "about", "like", "through",
|
|
25
|
+
"over", "before", "between", "under", "since", "without", "and", "or",
|
|
26
|
+
"but", "if", "than", "because", "as", "until", "while", "so",
|
|
27
|
+
"though", "both", "each", "when", "where", "why", "how", "not",
|
|
28
|
+
"now", "just", "more", "also", "here", "there", "then", "only",
|
|
29
|
+
"very", "well", "back", "still", "even", "much", "too", "such",
|
|
30
|
+
"never", "again", "most", "once", "off", "away", "down", "out",
|
|
31
|
+
"up", "test", "code", "data", "file", "line", "text", "word",
|
|
32
|
+
"number", "system", "program", "set", "run", "value", "name",
|
|
33
|
+
"type", "state", "end", "start"
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function generateLoremIpsum(targetTokens: number): string {
|
|
37
|
+
let tokens = 0;
|
|
38
|
+
let result = "";
|
|
39
|
+
|
|
40
|
+
while (tokens < targetTokens) {
|
|
41
|
+
const sentenceLength = 10 + Math.floor(Math.random() * 11);
|
|
42
|
+
let wordsInSentence = 0;
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < sentenceLength && tokens < targetTokens; i++) {
|
|
45
|
+
const word = ONE_TOKEN_WORDS[Math.floor(Math.random() * ONE_TOKEN_WORDS.length)];
|
|
46
|
+
result += word;
|
|
47
|
+
tokens++;
|
|
48
|
+
wordsInSentence++;
|
|
49
|
+
|
|
50
|
+
if (i === sentenceLength - 1 || tokens >= targetTokens) {
|
|
51
|
+
result += ". ";
|
|
52
|
+
} else {
|
|
53
|
+
result += " ";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Paragraph break every 5-8 sentences
|
|
58
|
+
if (wordsInSentence > 0 && Math.random() < 0.2 && tokens < targetTokens) {
|
|
59
|
+
result += "\n\n";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerLoremIpsumSkill(): void {
|
|
67
|
+
registerBundledSkill({
|
|
68
|
+
name: "lorem-ipsum",
|
|
69
|
+
description:
|
|
70
|
+
"Generate filler text for long context testing. Specify token count as argument (e.g., /lorem-ipsum 50000).",
|
|
71
|
+
argumentHint: "[token_count]",
|
|
72
|
+
userInvocable: true,
|
|
73
|
+
async getPromptForCommand(args) {
|
|
74
|
+
const parsed = parseInt(args);
|
|
75
|
+
|
|
76
|
+
if (args && (isNaN(parsed) || parsed <= 0)) {
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: "Invalid token count. Please provide a positive number (e.g., /lorem-ipsum 10000).",
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const targetTokens = parsed || 10000;
|
|
86
|
+
const cappedTokens = Math.min(targetTokens, 500_000);
|
|
87
|
+
|
|
88
|
+
if (cappedTokens < targetTokens) {
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: `Requested ${targetTokens} tokens, but capped at 500,000 for safety.\n\n${generateLoremIpsum(cappedTokens)}`,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const loremText = generateLoremIpsum(cappedTokens);
|
|
98
|
+
return [{ type: "text", text: loremText }];
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|