@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.
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/index.mjs +106603 -0
- package/package.json +64 -0
- package/src/commands/clear.ts +18 -0
- package/src/commands/crimes.ts +48 -0
- package/src/commands/feed.ts +20 -0
- package/src/commands/genz.ts +33 -0
- package/src/commands/help.ts +25 -0
- package/src/commands/init.ts +65 -0
- package/src/commands/mode.ts +22 -0
- package/src/commands/pet.ts +35 -0
- package/src/commands/provider.ts +46 -0
- package/src/commands/roast.ts +40 -0
- package/src/commands/vibe.ts +42 -0
- package/src/commands.ts +43 -0
- package/src/components/AsciiLogo.tsx +25 -0
- package/src/components/CommandSuggestions.tsx +78 -0
- package/src/components/Header.tsx +68 -0
- package/src/components/HighlightedCode.tsx +23 -0
- package/src/components/Message.tsx +43 -0
- package/src/components/ProviderWizard.tsx +278 -0
- package/src/components/Spinner.tsx +76 -0
- package/src/components/StatusBar.tsx +85 -0
- package/src/components/StructuredDiff.tsx +194 -0
- package/src/components/TextInput.tsx +144 -0
- package/src/components/messages/AssistantMessage.tsx +68 -0
- package/src/components/messages/ToolCallMessage.tsx +77 -0
- package/src/components/messages/ToolResultMessage.tsx +181 -0
- package/src/components/messages/UserMessage.tsx +32 -0
- package/src/components/permissions/PermissionCard.tsx +152 -0
- package/src/history.ts +27 -0
- package/src/hooks/useArrowKeyHistory.ts +0 -0
- package/src/hooks/useChat.ts +271 -0
- package/src/hooks/useDoublePress.ts +35 -0
- package/src/hooks/useTerminalSize.ts +24 -0
- package/src/hooks/useTextInput.ts +263 -0
- package/src/icons.ts +31 -0
- package/src/index.tsx +5 -0
- package/src/multi-agent/agent/agent.ts +33 -0
- package/src/multi-agent/orchestrator/orchestrator.ts +103 -0
- package/src/multi-agent/schemas.ts +12 -0
- package/src/multi-agent/types.ts +8 -0
- package/src/permissions.ts +54 -0
- package/src/pet.ts +239 -0
- package/src/screens/REPL.tsx +261 -0
- package/src/shortcuts.ts +37 -0
- package/src/skills/backend.ts +76 -0
- package/src/skills/cicd.ts +57 -0
- package/src/skills/colors.ts +72 -0
- package/src/skills/database.ts +55 -0
- package/src/skills/docker.ts +74 -0
- package/src/skills/frontend.ts +70 -0
- package/src/skills/git.ts +52 -0
- package/src/skills/testing.ts +73 -0
- package/src/skills/typography.ts +57 -0
- package/src/skills/uiux.ts +43 -0
- package/src/tools/AgentTool/prompt.ts +17 -0
- package/src/tools/AgentTool/tool.ts +22 -0
- package/src/tools/BashTool/prompt.ts +82 -0
- package/src/tools/BashTool/tool.ts +54 -0
- package/src/tools/FileEditTool/prompt.ts +13 -0
- package/src/tools/FileEditTool/tool.ts +39 -0
- package/src/tools/FileReadTool/prompt.ts +5 -0
- package/src/tools/FileReadTool/tool.ts +34 -0
- package/src/tools/FileWriteTool/prompt.ts +19 -0
- package/src/tools/FileWriteTool/tool.ts +34 -0
- package/src/tools/GlobTool/prompt.ts +11 -0
- package/src/tools/GlobTool/tool.ts +34 -0
- package/src/tools/GrepTool/prompt.ts +13 -0
- package/src/tools/GrepTool/tool.ts +41 -0
- package/src/tools/MemoryEditTool/prompt.ts +10 -0
- package/src/tools/MemoryEditTool/tool.ts +38 -0
- package/src/tools/MemoryReadTool/prompt.ts +9 -0
- package/src/tools/MemoryReadTool/tool.ts +47 -0
- package/src/tools/MemoryWriteTool/prompt.ts +10 -0
- package/src/tools/MemoryWriteTool/tool.ts +30 -0
- package/src/tools/OrchestratorTool/prompt.ts +26 -0
- package/src/tools/OrchestratorTool/tool.ts +20 -0
- package/src/tools/RecallTool/prompt.ts +13 -0
- package/src/tools/RecallTool/tool.ts +47 -0
- package/src/tools/ThinkTool/tool.ts +16 -0
- package/src/tools/WebFetchTool/prompt.ts +7 -0
- package/src/tools/WebFetchTool/tool.ts +33 -0
- package/src/tools/WebSearchTool/prompt.ts +8 -0
- package/src/tools/WebSearchTool/tool.ts +49 -0
- package/src/types.ts +124 -0
- package/src/utils/Cursor.ts +423 -0
- package/src/utils/PersistentShell.ts +306 -0
- package/src/utils/agent.ts +21 -0
- package/src/utils/chat.ts +21 -0
- package/src/utils/compaction.ts +71 -0
- package/src/utils/env.ts +11 -0
- package/src/utils/file.ts +42 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/imagePaste.ts +78 -0
- package/src/utils/json.ts +10 -0
- package/src/utils/llm.ts +65 -0
- package/src/utils/markdown.ts +258 -0
- package/src/utils/messages.ts +81 -0
- package/src/utils/model.ts +16 -0
- package/src/utils/plan.ts +26 -0
- package/src/utils/providers.ts +100 -0
- package/src/utils/ripgrep.ts +175 -0
- package/src/utils/session.ts +100 -0
- package/src/utils/skills.ts +26 -0
- package/src/utils/systemPrompt.ts +218 -0
- package/src/utils/theme.ts +110 -0
- package/src/utils/tools.ts +58 -0
- 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
|
+
}
|
package/src/shortcuts.ts
ADDED
|
@@ -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
|
+
`;
|