@ridit/milo 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 (111) hide show
  1. package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
  2. package/LICENSE +21 -0
  3. package/README.md +122 -0
  4. package/dist/index.mjs +106603 -0
  5. package/package.json +64 -0
  6. package/src/commands/clear.ts +18 -0
  7. package/src/commands/crimes.ts +48 -0
  8. package/src/commands/feed.ts +20 -0
  9. package/src/commands/genz.ts +33 -0
  10. package/src/commands/help.ts +25 -0
  11. package/src/commands/init.ts +65 -0
  12. package/src/commands/mode.ts +22 -0
  13. package/src/commands/pet.ts +35 -0
  14. package/src/commands/provider.ts +46 -0
  15. package/src/commands/roast.ts +40 -0
  16. package/src/commands/vibe.ts +42 -0
  17. package/src/commands.ts +43 -0
  18. package/src/components/AsciiLogo.tsx +25 -0
  19. package/src/components/CommandSuggestions.tsx +78 -0
  20. package/src/components/Header.tsx +68 -0
  21. package/src/components/HighlightedCode.tsx +23 -0
  22. package/src/components/Message.tsx +43 -0
  23. package/src/components/ProviderWizard.tsx +278 -0
  24. package/src/components/Spinner.tsx +76 -0
  25. package/src/components/StatusBar.tsx +85 -0
  26. package/src/components/StructuredDiff.tsx +194 -0
  27. package/src/components/TextInput.tsx +144 -0
  28. package/src/components/messages/AssistantMessage.tsx +68 -0
  29. package/src/components/messages/ToolCallMessage.tsx +77 -0
  30. package/src/components/messages/ToolResultMessage.tsx +181 -0
  31. package/src/components/messages/UserMessage.tsx +32 -0
  32. package/src/components/permissions/PermissionCard.tsx +152 -0
  33. package/src/history.ts +27 -0
  34. package/src/hooks/useArrowKeyHistory.ts +0 -0
  35. package/src/hooks/useChat.ts +271 -0
  36. package/src/hooks/useDoublePress.ts +35 -0
  37. package/src/hooks/useTerminalSize.ts +24 -0
  38. package/src/hooks/useTextInput.ts +263 -0
  39. package/src/icons.ts +31 -0
  40. package/src/index.tsx +5 -0
  41. package/src/multi-agent/agent/agent.ts +33 -0
  42. package/src/multi-agent/orchestrator/orchestrator.ts +103 -0
  43. package/src/multi-agent/schemas.ts +12 -0
  44. package/src/multi-agent/types.ts +8 -0
  45. package/src/permissions.ts +54 -0
  46. package/src/pet.ts +239 -0
  47. package/src/screens/REPL.tsx +261 -0
  48. package/src/shortcuts.ts +37 -0
  49. package/src/skills/backend.ts +76 -0
  50. package/src/skills/cicd.ts +57 -0
  51. package/src/skills/colors.ts +72 -0
  52. package/src/skills/database.ts +55 -0
  53. package/src/skills/docker.ts +74 -0
  54. package/src/skills/frontend.ts +70 -0
  55. package/src/skills/git.ts +52 -0
  56. package/src/skills/testing.ts +73 -0
  57. package/src/skills/typography.ts +57 -0
  58. package/src/skills/uiux.ts +43 -0
  59. package/src/tools/AgentTool/prompt.ts +17 -0
  60. package/src/tools/AgentTool/tool.ts +22 -0
  61. package/src/tools/BashTool/prompt.ts +82 -0
  62. package/src/tools/BashTool/tool.ts +54 -0
  63. package/src/tools/FileEditTool/prompt.ts +13 -0
  64. package/src/tools/FileEditTool/tool.ts +39 -0
  65. package/src/tools/FileReadTool/prompt.ts +5 -0
  66. package/src/tools/FileReadTool/tool.ts +34 -0
  67. package/src/tools/FileWriteTool/prompt.ts +19 -0
  68. package/src/tools/FileWriteTool/tool.ts +34 -0
  69. package/src/tools/GlobTool/prompt.ts +11 -0
  70. package/src/tools/GlobTool/tool.ts +34 -0
  71. package/src/tools/GrepTool/prompt.ts +13 -0
  72. package/src/tools/GrepTool/tool.ts +41 -0
  73. package/src/tools/MemoryEditTool/prompt.ts +10 -0
  74. package/src/tools/MemoryEditTool/tool.ts +38 -0
  75. package/src/tools/MemoryReadTool/prompt.ts +9 -0
  76. package/src/tools/MemoryReadTool/tool.ts +47 -0
  77. package/src/tools/MemoryWriteTool/prompt.ts +10 -0
  78. package/src/tools/MemoryWriteTool/tool.ts +30 -0
  79. package/src/tools/OrchestratorTool/prompt.ts +26 -0
  80. package/src/tools/OrchestratorTool/tool.ts +20 -0
  81. package/src/tools/RecallTool/prompt.ts +13 -0
  82. package/src/tools/RecallTool/tool.ts +47 -0
  83. package/src/tools/ThinkTool/tool.ts +16 -0
  84. package/src/tools/WebFetchTool/prompt.ts +7 -0
  85. package/src/tools/WebFetchTool/tool.ts +33 -0
  86. package/src/tools/WebSearchTool/prompt.ts +8 -0
  87. package/src/tools/WebSearchTool/tool.ts +49 -0
  88. package/src/types.ts +124 -0
  89. package/src/utils/Cursor.ts +423 -0
  90. package/src/utils/PersistentShell.ts +306 -0
  91. package/src/utils/agent.ts +21 -0
  92. package/src/utils/chat.ts +21 -0
  93. package/src/utils/compaction.ts +71 -0
  94. package/src/utils/env.ts +11 -0
  95. package/src/utils/file.ts +42 -0
  96. package/src/utils/format.ts +46 -0
  97. package/src/utils/imagePaste.ts +78 -0
  98. package/src/utils/json.ts +10 -0
  99. package/src/utils/llm.ts +65 -0
  100. package/src/utils/markdown.ts +258 -0
  101. package/src/utils/messages.ts +81 -0
  102. package/src/utils/model.ts +16 -0
  103. package/src/utils/plan.ts +26 -0
  104. package/src/utils/providers.ts +100 -0
  105. package/src/utils/ripgrep.ts +175 -0
  106. package/src/utils/session.ts +100 -0
  107. package/src/utils/skills.ts +26 -0
  108. package/src/utils/systemPrompt.ts +218 -0
  109. package/src/utils/theme.ts +110 -0
  110. package/src/utils/tools.ts +58 -0
  111. package/tsconfig.json +29 -0
package/src/pet.ts ADDED
@@ -0,0 +1,239 @@
1
+ import { readFile, writeFile, mkdir } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { PET_FILE } from "./utils/env";
4
+ import type { Pet } from "./types";
5
+
6
+ const XP_PER_TOOL: Record<string, number> = {
7
+ ThinkTool: 2,
8
+ GrepTool: 5,
9
+ GlobTool: 5,
10
+ FileReadTool: 5,
11
+ FileWriteTool: 15,
12
+ FileEditTool: 15,
13
+ BashTool: 10,
14
+ AgentTool: 25,
15
+ OrchestratorTool: 50,
16
+ WebSearchTool: 5,
17
+ WebFetchTool: 5,
18
+ RecallTool: 3,
19
+ MemoryReadTool: 2,
20
+ MemoryWriteTool: 5,
21
+ MemoryEditTool: 5,
22
+ };
23
+
24
+ const XP_TO_NEXT_BASE = 100;
25
+ const XP_SCALING = 1.4;
26
+
27
+ function calcXpToNext(level: number): number {
28
+ return Math.floor(XP_TO_NEXT_BASE * Math.pow(XP_SCALING, level - 1));
29
+ }
30
+
31
+ function calcMood(pet: Pet): Pet["mood"] {
32
+ if (pet.hunger >= 80) return "sad";
33
+ if (pet.hunger >= 50) return "sleepy";
34
+ return "happy";
35
+ }
36
+
37
+ function todayString(): string {
38
+ return new Date().toISOString().split("T")[0]!;
39
+ }
40
+
41
+ const DEFAULT_PET: Pet = {
42
+ level: 1,
43
+ xp: 0,
44
+ xpToNext: XP_TO_NEXT_BASE,
45
+ mood: "happy",
46
+ hunger: 0,
47
+ streak: 1,
48
+ lastActive: new Date(),
49
+ totalTasks: 0,
50
+ };
51
+
52
+ export const LEVEL_FLAVOR: Record<number, string> = {
53
+ 1: "still a kitten 🐱",
54
+ 2: "finding your paws 🐾",
55
+ 3: "curious cat energy 👀",
56
+ 5: "mid-tier cat energy 😼",
57
+ 7: "getting dangerous 😈",
58
+ 10: "absolute unit 😤",
59
+ 15: "senior dev cat 🧠",
60
+ 20: "legendary. feared by dogs 👑",
61
+ 30: "transcended. one with the terminal 👻",
62
+ };
63
+
64
+ export function getLevelFlavor(level: number): string {
65
+ const keys = Object.keys(LEVEL_FLAVOR)
66
+ .map(Number)
67
+ .sort((a, b) => b - a);
68
+ for (const key of keys) {
69
+ if (level >= key) return LEVEL_FLAVOR[key]!;
70
+ }
71
+ return LEVEL_FLAVOR[1]!;
72
+ }
73
+
74
+ export const LEVEL_UP_MESSAGES: Record<number, string> = {
75
+ 3: "purrrr... /roast unlocked 😼",
76
+ 5: "getting powerful... /vibe unlocked 😈",
77
+ 10: "absolute unit achieved... /crimes unlocked 😤",
78
+ };
79
+
80
+ export const UNLOCKED_AT: Record<string, number> = {
81
+ roast: 3,
82
+ vibe: 5,
83
+ crimes: 10,
84
+ };
85
+
86
+ export function isCommandUnlocked(commandName: string, level: number): boolean {
87
+ const required = UNLOCKED_AT[commandName];
88
+ if (required === undefined) return true;
89
+ return level >= required;
90
+ }
91
+
92
+ export async function readPet(): Promise<Pet> {
93
+ try {
94
+ const raw = await readFile(PET_FILE, "utf-8");
95
+ const parsed = JSON.parse(raw);
96
+ return {
97
+ ...DEFAULT_PET,
98
+ ...parsed,
99
+ lastActive: new Date(parsed.lastActive),
100
+ };
101
+ } catch {
102
+ return { ...DEFAULT_PET };
103
+ }
104
+ }
105
+
106
+ export async function writePet(pet: Pet): Promise<void> {
107
+ await mkdir(dirname(PET_FILE), { recursive: true });
108
+ await writeFile(PET_FILE, JSON.stringify(pet, null, 2), "utf-8");
109
+ }
110
+
111
+ export type AwardXPResult = {
112
+ pet: Pet;
113
+ leveledUp: boolean;
114
+ oldLevel: number;
115
+ };
116
+
117
+ export async function awardXP(toolName: string): Promise<AwardXPResult> {
118
+ const pet = await readPet();
119
+ const oldLevel = pet.level;
120
+ const xp = XP_PER_TOOL[toolName] ?? 1;
121
+
122
+ const today = todayString();
123
+ const lastActive = pet.lastActive.toISOString().split("T")[0]!;
124
+ const yesterday = new Date(Date.now() - 86400000)
125
+ .toISOString()
126
+ .split("T")[0]!;
127
+ const streak =
128
+ lastActive === today
129
+ ? pet.streak
130
+ : lastActive === yesterday
131
+ ? pet.streak + 1
132
+ : 1;
133
+
134
+ const hunger = Math.min(100, pet.hunger + 1);
135
+
136
+ let newXp = pet.xp + xp;
137
+ let level = pet.level;
138
+ let xpToNext = pet.xpToNext;
139
+
140
+ while (newXp >= xpToNext) {
141
+ newXp -= xpToNext;
142
+ level++;
143
+ xpToNext = calcXpToNext(level);
144
+ }
145
+
146
+ const updated: Pet = {
147
+ level,
148
+ xp: newXp,
149
+ xpToNext,
150
+ hunger,
151
+ streak,
152
+ lastActive: new Date(),
153
+ totalTasks: pet.totalTasks + 1,
154
+ mood: "happy",
155
+ };
156
+
157
+ updated.mood = calcMood(updated);
158
+ await writePet(updated);
159
+
160
+ return { pet: updated, leveledUp: level > oldLevel, oldLevel };
161
+ }
162
+
163
+ export async function feedPet(): Promise<Pet> {
164
+ const pet = await readPet();
165
+ const updated: Pet = {
166
+ ...pet,
167
+ hunger: 0,
168
+ mood: "happy",
169
+ lastActive: new Date(),
170
+ };
171
+ await writePet(updated);
172
+ return updated;
173
+ }
174
+
175
+ export function getMoodEmoji(mood: Pet["mood"]): string {
176
+ switch (mood) {
177
+ case "happy":
178
+ return "😺";
179
+ case "sleepy":
180
+ return "😴";
181
+ case "sad":
182
+ return "😿";
183
+ }
184
+ }
185
+
186
+ export function renderXpBar(xp: number, xpToNext: number, width = 20): string {
187
+ const filled = Math.floor((xp / xpToNext) * width);
188
+ return "█".repeat(filled) + "░".repeat(width - filled);
189
+ }
190
+
191
+ export function getSpinnerPool(level: number): string[] {
192
+ const base = [
193
+ "Sniffing… 🐱",
194
+ "Pawing… 🐾",
195
+ "Purring… 😌",
196
+ "Staring… 👁️",
197
+ "Vibing… 🎵",
198
+ "We move 🫡",
199
+ "Cooking… 🍳",
200
+ "Napping… 💤",
201
+ "Meow meow 🐱",
202
+ "Understood 📋",
203
+ ];
204
+
205
+ const mid = [
206
+ ...base,
207
+ "Feral rn 🐈",
208
+ "Cat brain 🧠",
209
+ "No cap 🧢",
210
+ "Fr fr 😭",
211
+ "Slay mode 💅",
212
+ "Pouncing… 🏹",
213
+ "Zooming… 💨",
214
+ "Plotting… 😼",
215
+ ];
216
+
217
+ const feral = [
218
+ ...mid,
219
+ "Judging… 😾",
220
+ "Hairball incoming 💀",
221
+ "Touch grass? 🌿",
222
+ "Midnight mode 🌙",
223
+ "Ate diff 🍽️",
224
+ "Knocking over 🫡",
225
+ "Absolutely unhinged 😈",
226
+ "Sending it 🚀",
227
+ "No thoughts 🫥",
228
+ "Grooming… 🧹",
229
+ "Box sitting 📦",
230
+ "Tail twitching 👀",
231
+ "Ears perked 📡",
232
+ "Claw sharpening ⚡",
233
+ "Paw paw 🐾",
234
+ ];
235
+
236
+ if (level >= 6) return feral;
237
+ if (level >= 3) return mid;
238
+ return base;
239
+ }
@@ -0,0 +1,261 @@
1
+ import React, { useState, type JSX } from "react";
2
+ import { Box, Text, Static, useInput } from "ink";
3
+ import { getHistory, addToHistory } from "../history";
4
+ import TextInput from "../components/TextInput";
5
+ import { Spinner } from "../components/Spinner";
6
+ import { useTerminalSize } from "../hooks/useTerminalSize";
7
+ import { useChat } from "../hooks/useChat";
8
+ import { getTheme } from "../utils/theme";
9
+ import { Message } from "../components/Message";
10
+ import { pointer, line } from "../icons";
11
+ import { Header } from "../components/Header";
12
+ import { ProviderWizard } from "../components/ProviderWizard";
13
+ import {
14
+ CommandSuggestions,
15
+ getMatchingCommands,
16
+ } from "../components/CommandSuggestions";
17
+ import type { ChatMessage } from "../types";
18
+ import { StatusBar } from "../components/StatusBar";
19
+ import { findShortcut } from "../shortcuts";
20
+ import { PermissionCard } from "../components/permissions/PermissionCard";
21
+
22
+ const HEADER_ITEM = [{ id: "header", type: "header" as const }];
23
+
24
+ type StaticItem =
25
+ | { id: string; type: "header" }
26
+ | { id: string; type: "message"; msg: ChatMessage; index: number };
27
+
28
+ export default function REPL(): JSX.Element {
29
+ const { columns } = useTerminalSize();
30
+ const [value, setValue] = useState("");
31
+ const [cursorOffset, setCursorOffset] = useState(0);
32
+ const [selectedIndex, setSelectedIndex] = useState(0);
33
+ const [history, setHistory] = useState<string[]>([]);
34
+ const [historyIndex, setHistoryIndex] = useState(0);
35
+ const [lastTypedInput, setLastTypedInput] = useState("");
36
+ const [modelLabel, setModelLabel] = useState("no model");
37
+ const {
38
+ messages,
39
+ loading,
40
+ submit,
41
+ mode,
42
+ setMode,
43
+ clearMessages,
44
+ decide,
45
+ pendingPermission,
46
+ pendingWizard,
47
+ closeWizard,
48
+ } = useChat();
49
+
50
+ React.useEffect(() => {
51
+ import("../utils/model")
52
+ .then((m) => m.getModel())
53
+ .then(({ modelId }) => setModelLabel(modelId))
54
+ .catch(() => {});
55
+ }, [pendingWizard]);
56
+
57
+ function onSubmit(input: string) {
58
+ if (!input.trim() || loading) return;
59
+ addToHistory(input.trim()).catch(() => {});
60
+ setHistory((prev) =>
61
+ prev[0] === input.trim() ? prev : [input.trim(), ...prev].slice(0, 100),
62
+ );
63
+ setHistoryIndex(0);
64
+ setLastTypedInput("");
65
+ submit(input);
66
+ setValue("");
67
+ setCursorOffset(0);
68
+ setSelectedIndex(0);
69
+ }
70
+
71
+ React.useEffect(() => {
72
+ setSelectedIndex(0);
73
+ }, [value]);
74
+
75
+ React.useEffect(() => {
76
+ getHistory()
77
+ .then(setHistory)
78
+ .catch(() => {});
79
+ }, []);
80
+
81
+ function onHistoryUp() {
82
+ if (historyIndex < history.length) {
83
+ if (historyIndex === 0 && value.trim() !== "") {
84
+ setLastTypedInput(value);
85
+ }
86
+ const newIndex = historyIndex + 1;
87
+ setHistoryIndex(newIndex);
88
+ const entry = history[historyIndex] ?? "";
89
+ setValue(entry);
90
+ setCursorOffset(entry.length);
91
+ }
92
+ }
93
+
94
+ function onHistoryDown() {
95
+ if (historyIndex > 1) {
96
+ const newIndex = historyIndex - 1;
97
+ setHistoryIndex(newIndex);
98
+ const entry = history[newIndex - 1] ?? "";
99
+ setValue(entry);
100
+ setCursorOffset(entry.length);
101
+ } else if (historyIndex === 1) {
102
+ setHistoryIndex(0);
103
+ setValue(lastTypedInput);
104
+ setCursorOffset(lastTypedInput.length);
105
+ }
106
+ }
107
+
108
+ function onHistoryReset() {
109
+ setHistoryIndex(0);
110
+ setLastTypedInput("");
111
+ }
112
+
113
+ useInput(
114
+ (input, key) => {
115
+ if (pendingWizard) return;
116
+ if (key.tab && value.startsWith("/")) {
117
+ const matches = getMatchingCommands(value);
118
+ if (matches.length === 0) return;
119
+ const match = matches[selectedIndex] ?? matches[0];
120
+ if (match) {
121
+ const completed = "/" + match.userFacingName() + " ";
122
+ setValue(completed);
123
+ setCursorOffset(completed.length);
124
+ }
125
+ }
126
+ if (key.upArrow && value.startsWith("/")) {
127
+ setSelectedIndex((i) => Math.max(0, i - 1));
128
+ }
129
+ if (key.downArrow && value.startsWith("/")) {
130
+ const matches = getMatchingCommands(value);
131
+ setSelectedIndex((i) => Math.min(matches.length - 1, i + 1));
132
+ }
133
+ const shortcut = findShortcut(input, key);
134
+ if (shortcut) {
135
+ shortcut.action({ clearMessages, mode, setMode });
136
+ }
137
+ },
138
+ { isActive: !loading },
139
+ );
140
+
141
+ const staticMessages = messages.filter(
142
+ (m) => !(m.type === "tool_call" && (m as any).isOrchestrated),
143
+ );
144
+
145
+ const orchestratedMessages = messages.filter(
146
+ (m) =>
147
+ (m.type === "tool_call" || m.type === "tool_result") &&
148
+ (m as any).isOrchestrated,
149
+ );
150
+
151
+ const orchestratedDone = orchestratedMessages.filter(
152
+ (m) => m.type === "tool_result",
153
+ );
154
+
155
+ const orchestratedTotal = messages.filter(
156
+ (m) => m.type === "tool_call" && (m as any).isOrchestrated,
157
+ );
158
+
159
+ const isOrchestrating = orchestratedTotal.length > 0 && loading;
160
+
161
+ const staticItems: StaticItem[] = [
162
+ ...HEADER_ITEM,
163
+ ...staticMessages.map((msg, index) => ({
164
+ id: msg.id,
165
+ type: "message" as const,
166
+ msg,
167
+ index,
168
+ })),
169
+ ];
170
+
171
+ const orchestratorHeight = isOrchestrating
172
+ ? orchestratedTotal.slice(-4).length + 1
173
+ : 0;
174
+
175
+ return (
176
+ <Box flexDirection="column" height="100%">
177
+ <Static items={staticItems}>
178
+ {(item) => {
179
+ if (item.type === "header") return <Header key="header" />;
180
+ return (
181
+ <Message key={item.id} msg={item.msg} isFirst={item.index === 0} />
182
+ );
183
+ }}
184
+ </Static>
185
+
186
+ <Box
187
+ flexDirection="column"
188
+ marginLeft={2}
189
+ minHeight={orchestratorHeight}
190
+ marginTop={isOrchestrating ? 1 : 0}
191
+ >
192
+ {isOrchestrating && (
193
+ <>
194
+ <Text color={getTheme().primary}>
195
+ ⚡ agents{" "}
196
+ <Text color={getTheme().success}>{orchestratedDone.length}</Text>
197
+ <Text color={getTheme().secondaryText}>
198
+ /{orchestratedTotal.length} done
199
+ </Text>
200
+ </Text>
201
+ {orchestratedTotal.slice(-4).map((m) => {
202
+ const isDone = orchestratedMessages.some(
203
+ (r) => r.type === "tool_result" && r.id === m.id,
204
+ );
205
+ const task = String(
206
+ (m.type === "tool_call" ? (m.input as any)?.task : "") ?? "",
207
+ ).slice(0, columns - 12);
208
+ return (
209
+ <Box key={m.id} flexDirection="row" gap={1}>
210
+ <Text
211
+ color={
212
+ isDone ? getTheme().success : getTheme().secondaryText
213
+ }
214
+ >
215
+ {isDone ? "✔" : "◆"}
216
+ </Text>
217
+ <Text color={getTheme().secondaryText} dimColor>
218
+ {task}
219
+ </Text>
220
+ </Box>
221
+ );
222
+ })}
223
+ </>
224
+ )}
225
+ </Box>
226
+
227
+ <Box minHeight={2}>{loading && <Spinner />}</Box>
228
+
229
+ <Box flexDirection="column">
230
+ <Text color={getTheme().border}>{line.repeat(columns)}</Text>
231
+ {pendingPermission ? (
232
+ <PermissionCard permission={pendingPermission} onDecide={decide} />
233
+ ) : pendingWizard ? (
234
+ <ProviderWizard mode={pendingWizard} onDone={closeWizard} />
235
+ ) : (
236
+ <Box paddingX={1}>
237
+ <Text color={getTheme().primary}>{pointer} </Text>
238
+ <TextInput
239
+ value={value}
240
+ onChange={setValue}
241
+ onSubmit={onSubmit}
242
+ onExit={() => process.exit(0)}
243
+ columns={columns - 6}
244
+ cursorOffset={cursorOffset}
245
+ onChangeCursorOffset={setCursorOffset}
246
+ placeholder="ask milo anything..."
247
+ isDimmed={loading}
248
+ focus={!loading && !pendingPermission && !pendingWizard}
249
+ onHistoryUp={onHistoryUp}
250
+ onHistoryDown={onHistoryDown}
251
+ onHistoryReset={onHistoryReset}
252
+ />
253
+ </Box>
254
+ )}
255
+ <Text color={getTheme().border}>{line.repeat(columns)}</Text>
256
+ <CommandSuggestions query={value} selectedIndex={selectedIndex} />
257
+ <StatusBar model={modelLabel} mode={mode} thinking={loading} />
258
+ </Box>
259
+ </Box>
260
+ );
261
+ }
@@ -0,0 +1,37 @@
1
+ import type { Mode } from "./types";
2
+
3
+ type Shortcut = {
4
+ key: string;
5
+ ctrl?: boolean;
6
+ description: string;
7
+ action: (...args: any[]) => void;
8
+ };
9
+
10
+ const registry: Shortcut[] = [];
11
+
12
+ export function registerShortcut(shortcut: Shortcut): void {
13
+ registry.push(shortcut);
14
+ }
15
+
16
+ export function findShortcut(
17
+ input: string,
18
+ key: { ctrl?: boolean },
19
+ ): Shortcut | undefined {
20
+ return registry.find((s) => s.key === input && !!s.ctrl === !!key.ctrl);
21
+ }
22
+
23
+ export function getShortcuts(): Shortcut[] {
24
+ return registry;
25
+ }
26
+
27
+ // register built-in shortcuts
28
+ registerShortcut({
29
+ key: "t",
30
+ ctrl: true,
31
+ description: "cycle mode (agent → plan → chat)",
32
+ action: ({ mode, setMode }) => {
33
+ const cycle: Mode[] = ["agent", "plan", "chat"];
34
+ const next = cycle[(cycle.indexOf(mode) + 1) % cycle.length];
35
+ if (next) setMode(next);
36
+ },
37
+ });
@@ -0,0 +1,76 @@
1
+ export const BACKEND_SKILL = `
2
+ ## Skill: Backend Development
3
+
4
+ ### API Design
5
+ - RESTful conventions:
6
+ \`\`\`
7
+ GET /users → list users
8
+ GET /users/:id → get user
9
+ POST /users → create user
10
+ PUT /users/:id → replace user
11
+ PATCH /users/:id → update user
12
+ DELETE /users/:id → delete user
13
+ \`\`\`
14
+ - Use proper HTTP status codes:
15
+ - 200 OK, 201 Created, 204 No Content
16
+ - 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable
17
+ - 500 Internal Server Error
18
+ - Consistent error shape:
19
+ \`\`\`ts
20
+ { error: string; code?: string; details?: unknown }
21
+ \`\`\`
22
+
23
+ ### Validation
24
+ - Always validate at the boundary (route handler), never trust input:
25
+ \`\`\`ts
26
+ import { z } from 'zod';
27
+
28
+ const CreateUserSchema = z.object({
29
+ email: z.string().email(),
30
+ name: z.string().min(1).max(100),
31
+ age: z.number().int().min(0).max(150).optional(),
32
+ });
33
+
34
+ app.post('/users', async (req, res) => {
35
+ const result = CreateUserSchema.safeParse(req.body);
36
+ if (!result.success) {
37
+ return res.status(422).json({ error: 'Validation failed', details: result.error.flatten() });
38
+ }
39
+ // result.data is now typed and safe
40
+ });
41
+ \`\`\`
42
+
43
+ ### Error Handling
44
+ - Never expose stack traces to clients
45
+ - Use a global error handler:
46
+ \`\`\`ts
47
+ app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
48
+ console.error(err);
49
+ res.status(500).json({ error: 'Internal server error' });
50
+ });
51
+ \`\`\`
52
+ - Wrap async handlers:
53
+ \`\`\`ts
54
+ const asyncHandler = (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) =>
55
+ Promise.resolve(fn(req, res, next)).catch(next);
56
+ \`\`\`
57
+
58
+ ### Architecture
59
+ - Keep route handlers thin:
60
+ \`\`\`ts
61
+ // ❌ wrong — logic in handler
62
+ app.post('/users', async (req, res) => {
63
+ const hash = await bcrypt.hash(req.body.password, 10);
64
+ const user = await db.insert(users).values({ ...req.body, password: hash });
65
+ res.json(user);
66
+ });
67
+
68
+ // ✅ correct — handler delegates to service
69
+ app.post('/users', asyncHandler(async (req, res) => {
70
+ const user = await userService.create(req.body);
71
+ res.status(201).json(user);
72
+ }));
73
+ \`\`\`
74
+ - Secrets via environment variables only — never hardcode
75
+ - Use middleware for auth, logging, rate limiting
76
+ `;
@@ -0,0 +1,57 @@
1
+ export const CICD_SKILL = `
2
+ ## Skill: CI/CD
3
+
4
+ ### GitHub Actions Structure
5
+ \`\`\`yaml
6
+ name: CI
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ pull_request:
12
+ branches: [main]
13
+
14
+ jobs:
15
+ check:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: actions/setup-node@v4
21
+ with:
22
+ node-version: 20
23
+ cache: 'npm'
24
+
25
+ - run: npm ci
26
+ - run: npm run type-check
27
+ - run: npm run lint
28
+ - run: npm run test
29
+ - run: npm run build
30
+ \`\`\`
31
+
32
+ ### Best Practices
33
+ - Fail fast — run quick checks first (lint, type-check) before slow ones (tests, build)
34
+ - Cache dependencies:
35
+ \`\`\`yaml
36
+ - uses: actions/cache@v4
37
+ with:
38
+ path: ~/.npm
39
+ key: \${{ runner.os }}-node-\${{ hashFiles('**/package-lock.json') }}
40
+ \`\`\`
41
+ - Use secrets for credentials — never hardcode:
42
+ \`\`\`yaml
43
+ env:
44
+ DATABASE_URL: \${{ secrets.DATABASE_URL }}
45
+ API_KEY: \${{ secrets.API_KEY }}
46
+ \`\`\`
47
+ - Deploy to staging before production
48
+ - Always have a rollback plan — keep previous build artifacts
49
+ - Use semantic versioning for releases with automated changelog
50
+
51
+ ### Deployment Pattern
52
+ \`\`\`
53
+ PR opened → lint + test
54
+ PR merged to main → test + build + deploy to staging
55
+ Tag pushed (v*) → deploy to production
56
+ \`\`\`
57
+ `;