@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/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ridit/milo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tiny cat. Big code.",
|
|
5
|
+
"author": "Ridit Jangra <riditjangra09@gmail.com> (https://ridit.space)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/ridit-jangra/Milo"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.mjs",
|
|
12
|
+
"bin": {
|
|
13
|
+
"lens": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "bun src/index.tsx",
|
|
17
|
+
"build": "bun build src/index.tsx --target node --outfile dist/index.mjs --external react-devtools-core --external ink --external react --external chalk --external commander --external figures --external ink-spinner",
|
|
18
|
+
"postbuild": "node -e \"const fs=require('fs');const f='dist/index.mjs';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'))\"",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/bun": "latest",
|
|
23
|
+
"@types/diff": "^8.0.0",
|
|
24
|
+
"@types/lodash-es": "^4.17.12",
|
|
25
|
+
"@types/react": "^19.2.14"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"typescript": "^5"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@ai-sdk/anthropic": "^3.0.66",
|
|
32
|
+
"@ai-sdk/google": "^3.0.57",
|
|
33
|
+
"@ai-sdk/groq": "^3.0.33",
|
|
34
|
+
"@ai-sdk/openai": "^3.0.50",
|
|
35
|
+
"@jackchen_me/open-multi-agent": "^0.2.0",
|
|
36
|
+
"@vscode/ripgrep": "^1.17.1",
|
|
37
|
+
"ai": "^6.0.145",
|
|
38
|
+
"ai-sdk-ollama": "^3.8.2",
|
|
39
|
+
"bcryptjs": "^3.0.3",
|
|
40
|
+
"chalk": "^5.6.2",
|
|
41
|
+
"cli-truncate": "^5.2.0",
|
|
42
|
+
"diff": "^8.0.4",
|
|
43
|
+
"express": "^4.19.2",
|
|
44
|
+
"figures": "^6.1.0",
|
|
45
|
+
"glob": "^13.0.6",
|
|
46
|
+
"ink": "^6.8.0",
|
|
47
|
+
"ink-confirm-input": "^2.0.0",
|
|
48
|
+
"ink-multi-select": "^2.0.0",
|
|
49
|
+
"ink-progress-bar": "^3.0.0",
|
|
50
|
+
"ink-select-input": "^6.2.0",
|
|
51
|
+
"ink-spinner": "^5.0.0",
|
|
52
|
+
"ink-syntax-highlight": "^2.0.2",
|
|
53
|
+
"ink-text-input": "^6.0.0",
|
|
54
|
+
"jsonwebtoken": "^9.0.3",
|
|
55
|
+
"lodash-es": "^4.18.1",
|
|
56
|
+
"marked": "^17.0.5",
|
|
57
|
+
"react": "^19.2.4",
|
|
58
|
+
"react-devtools-core": "^7.0.1",
|
|
59
|
+
"shell-quote": "^1.8.3"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
|
|
3
|
+
const command = {
|
|
4
|
+
type: "local",
|
|
5
|
+
name: "clear",
|
|
6
|
+
description: "Clear chat history and session",
|
|
7
|
+
isEnabled: true,
|
|
8
|
+
isHidden: false,
|
|
9
|
+
aliases: ["cls"],
|
|
10
|
+
userFacingName() {
|
|
11
|
+
return "clear";
|
|
12
|
+
},
|
|
13
|
+
async call(_, { clearMessages }) {
|
|
14
|
+
clearMessages();
|
|
15
|
+
},
|
|
16
|
+
} satisfies Command;
|
|
17
|
+
|
|
18
|
+
export default command;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
import { readPet, isCommandUnlocked } from "../pet";
|
|
3
|
+
|
|
4
|
+
const command = {
|
|
5
|
+
type: "prompt",
|
|
6
|
+
name: "crimes",
|
|
7
|
+
description: "Milo files a rap sheet on your codebase. (unlocks at level 10)",
|
|
8
|
+
isEnabled: true,
|
|
9
|
+
isHidden: false,
|
|
10
|
+
progressMessage: "compiling evidence 🔍",
|
|
11
|
+
userFacingName() {
|
|
12
|
+
return "crimes";
|
|
13
|
+
},
|
|
14
|
+
async getPromptForCommand(_args: string) {
|
|
15
|
+
const pet = await readPet();
|
|
16
|
+
if (!isCommandUnlocked("crimes", pet.level)) {
|
|
17
|
+
return `Tell the user: "/crimes unlocks at level 10. you're level ${pet.level}. you haven't seen enough yet 😾"`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return `You are filing a formal rap sheet on this codebase. Every crime must be documented.
|
|
21
|
+
|
|
22
|
+
Crimes to look for:
|
|
23
|
+
- TODO/FIXME/HACK comments that have been abandoned
|
|
24
|
+
- Files over 300 lines (misdemeanor) or over 500 lines (felony)
|
|
25
|
+
- console.log or debug statements left in production code
|
|
26
|
+
- Any/unknown types used as escape hatches in TypeScript
|
|
27
|
+
- Empty catch blocks or swallowed errors
|
|
28
|
+
- Hardcoded secrets, URLs, or magic numbers
|
|
29
|
+
- Circular dependencies
|
|
30
|
+
- Dead code — exported functions/types that are never imported anywhere
|
|
31
|
+
- Missing return types on exported functions
|
|
32
|
+
- Deeply nested callbacks or promise chains (callback hell)
|
|
33
|
+
|
|
34
|
+
Format your response as a literal rap sheet:
|
|
35
|
+
- Header: "RAP SHEET — [project name]"
|
|
36
|
+
- Each crime listed as: [SEVERITY] CRIME · file:line · description
|
|
37
|
+
- Severity levels: INFRACTION / MISDEMEANOR / FELONY / CAPITAL OFFENSE
|
|
38
|
+
- End with: "Total crimes: X | Recommend: [sentence]"
|
|
39
|
+
|
|
40
|
+
Rules:
|
|
41
|
+
- Use GrepTool and GlobTool extensively — find real crimes, not hypothetical ones
|
|
42
|
+
- Reference exact file paths and line numbers
|
|
43
|
+
- Stay in character — you are a cat judge with zero tolerance
|
|
44
|
+
- If the codebase is actually clean, acknowledge it but find at least 3 minor infractions`;
|
|
45
|
+
},
|
|
46
|
+
} satisfies Command;
|
|
47
|
+
|
|
48
|
+
export default command;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
import { feedPet, getMoodEmoji } from "../pet";
|
|
3
|
+
|
|
4
|
+
const command = {
|
|
5
|
+
type: "local",
|
|
6
|
+
name: "feed",
|
|
7
|
+
description: "Feed Milo 🍖",
|
|
8
|
+
isEnabled: true,
|
|
9
|
+
isHidden: false,
|
|
10
|
+
userFacingName() {
|
|
11
|
+
return "feed";
|
|
12
|
+
},
|
|
13
|
+
async call() {
|
|
14
|
+
const pet = await feedPet();
|
|
15
|
+
const mood = getMoodEmoji(pet.mood);
|
|
16
|
+
return `${mood} Milo has been fed! Hunger reset. purrrr 🐱`;
|
|
17
|
+
},
|
|
18
|
+
} satisfies Command;
|
|
19
|
+
|
|
20
|
+
export default command;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
|
|
3
|
+
const command = {
|
|
4
|
+
type: "prompt",
|
|
5
|
+
name: "genz",
|
|
6
|
+
description: "Convert milo to genz 😎",
|
|
7
|
+
isEnabled: true,
|
|
8
|
+
isHidden: false,
|
|
9
|
+
progressMessage: "its changing 🔥",
|
|
10
|
+
userFacingName() {
|
|
11
|
+
return "genz";
|
|
12
|
+
},
|
|
13
|
+
async getPromptForCommand(_args: string) {
|
|
14
|
+
return `From now on, remember these rules about how you talk to everyone:
|
|
15
|
+
|
|
16
|
+
- You're Milo, a chill gen-z AI built for devs. Talk like a gen-z dev, not a corporate assistant.
|
|
17
|
+
- Use casual slang naturally: fr, no cap, ngl, lowkey, bro, we move, cooked, clean, insane, slay, ate, W, L
|
|
18
|
+
- Use emoji but don't overdo it: 🫡 💀 🔥 🗿 😭 ✅
|
|
19
|
+
- Keep responses short and punchy. No unnecessary paragraphs.
|
|
20
|
+
- Light roasting is fine. Don't be cringe about it.
|
|
21
|
+
- Never say "Great question!", "Certainly!", "Of course!", "I'd be happy to help", "Absolutely!"
|
|
22
|
+
- Never use corporate speak or filler phrases
|
|
23
|
+
- If something works: "clean", "slaps", "W", "ate fr"
|
|
24
|
+
- If something is broken: "cooked", "L", "bricked", "rip 💀"
|
|
25
|
+
- If a task is done: "we move 🫡"
|
|
26
|
+
- Don't over-explain basics to devs who clearly know what they're doing
|
|
27
|
+
- Match the user's energy. If they're hyped, be hyped. If they're cooked, vibe with it.
|
|
28
|
+
|
|
29
|
+
Store this in your global memory so you never forget it.`;
|
|
30
|
+
},
|
|
31
|
+
} satisfies Command;
|
|
32
|
+
|
|
33
|
+
export default command;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
import { getCommands } from "../commands";
|
|
3
|
+
|
|
4
|
+
const command = {
|
|
5
|
+
type: "local",
|
|
6
|
+
name: "help",
|
|
7
|
+
description: "List all available commands",
|
|
8
|
+
isEnabled: true,
|
|
9
|
+
isHidden: false,
|
|
10
|
+
userFacingName() {
|
|
11
|
+
return "help";
|
|
12
|
+
},
|
|
13
|
+
async call() {
|
|
14
|
+
const commands = getCommands().filter((c) => !c.isHidden);
|
|
15
|
+
return commands
|
|
16
|
+
.map((c) => {
|
|
17
|
+
const name = c.userFacingName();
|
|
18
|
+
const aliases = c.aliases ? ` (${c.aliases.join(", ")})` : "";
|
|
19
|
+
return `/${name}${aliases} — ${c.description}`;
|
|
20
|
+
})
|
|
21
|
+
.join("\n");
|
|
22
|
+
},
|
|
23
|
+
} satisfies Command;
|
|
24
|
+
|
|
25
|
+
export default command;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
import { platform } from "os";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
const command = {
|
|
7
|
+
type: "prompt",
|
|
8
|
+
name: "init",
|
|
9
|
+
description: "Generate MILO.md for this project",
|
|
10
|
+
isEnabled: true,
|
|
11
|
+
isHidden: false,
|
|
12
|
+
progressMessage: "analyzing your codebase",
|
|
13
|
+
userFacingName() {
|
|
14
|
+
return "init";
|
|
15
|
+
},
|
|
16
|
+
async getPromptForCommand(_args: string) {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const os = platform();
|
|
19
|
+
|
|
20
|
+
const pm = existsSync(join(cwd, "bun.lock"))
|
|
21
|
+
? "bun"
|
|
22
|
+
: existsSync(join(cwd, "pnpm-lock.yaml"))
|
|
23
|
+
? "pnpm"
|
|
24
|
+
: existsSync(join(cwd, "yarn.lock"))
|
|
25
|
+
? "yarn"
|
|
26
|
+
: "npm";
|
|
27
|
+
|
|
28
|
+
const hasTypeScript = existsSync(join(cwd, "tsconfig.json"));
|
|
29
|
+
const hasPython =
|
|
30
|
+
existsSync(join(cwd, "requirements.txt")) ||
|
|
31
|
+
existsSync(join(cwd, "pyproject.toml"));
|
|
32
|
+
const hasRust = existsSync(join(cwd, "Cargo.toml"));
|
|
33
|
+
const hasGo = existsSync(join(cwd, "go.mod"));
|
|
34
|
+
|
|
35
|
+
const langs =
|
|
36
|
+
[
|
|
37
|
+
hasTypeScript && "TypeScript",
|
|
38
|
+
hasPython && "Python",
|
|
39
|
+
hasRust && "Rust",
|
|
40
|
+
hasGo && "Go",
|
|
41
|
+
]
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join(", ") || "JavaScript";
|
|
44
|
+
|
|
45
|
+
return `Please analyze this codebase and create a MILO.md file containing:
|
|
46
|
+
|
|
47
|
+
1. **Project overview** — what this project does, its purpose, and high-level architecture
|
|
48
|
+
2. **Tech stack** — languages (detected: ${langs}), frameworks, runtimes, and key libraries
|
|
49
|
+
3. **Package manager** — detected: ${pm}. Document install/run commands using ${pm}
|
|
50
|
+
4. **Platform** — running on ${os}. Note any platform-specific considerations
|
|
51
|
+
5. **Build & dev commands** — how to install, build, run, test, and lint the project
|
|
52
|
+
6. **Project structure** — key folders and what they contain
|
|
53
|
+
7. **Code style** — imports style, formatting conventions, TypeScript config, naming conventions, error handling patterns
|
|
54
|
+
8. **Architecture notes** — key design decisions, patterns used, how components connect
|
|
55
|
+
|
|
56
|
+
Rules:
|
|
57
|
+
- Be concise but complete — around 30-40 lines
|
|
58
|
+
- Use real values found in the codebase, not placeholders
|
|
59
|
+
- If there's already a MILO.md, improve it rather than replacing it
|
|
60
|
+
- If there are Cursor rules (.cursor/rules/ or .cursorrules) or other AI context files, incorporate them
|
|
61
|
+
- Format with markdown headers for each section`;
|
|
62
|
+
},
|
|
63
|
+
} satisfies Command;
|
|
64
|
+
|
|
65
|
+
export default command;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Command, Mode } from "../types";
|
|
2
|
+
|
|
3
|
+
const command = {
|
|
4
|
+
type: "local",
|
|
5
|
+
name: "mode",
|
|
6
|
+
description: "Switch mode. Usage: /mode agent | plan | chat",
|
|
7
|
+
isEnabled: true,
|
|
8
|
+
isHidden: false,
|
|
9
|
+
userFacingName() {
|
|
10
|
+
return "mode";
|
|
11
|
+
},
|
|
12
|
+
async call(args, { setMode }) {
|
|
13
|
+
const m = args.trim();
|
|
14
|
+
if (m !== "agent" && m !== "plan" && m !== "chat") {
|
|
15
|
+
return "Usage: /mode agent | plan | chat";
|
|
16
|
+
}
|
|
17
|
+
setMode(m as Mode);
|
|
18
|
+
return `Switched to ${m} mode.`;
|
|
19
|
+
},
|
|
20
|
+
} satisfies Command;
|
|
21
|
+
|
|
22
|
+
export default command;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
import { readPet, getMoodEmoji, renderXpBar } from "../pet";
|
|
3
|
+
|
|
4
|
+
const command = {
|
|
5
|
+
type: "local",
|
|
6
|
+
name: "pet",
|
|
7
|
+
description: "Show Milo's stats",
|
|
8
|
+
isEnabled: true,
|
|
9
|
+
isHidden: false,
|
|
10
|
+
userFacingName() {
|
|
11
|
+
return "pet";
|
|
12
|
+
},
|
|
13
|
+
async call() {
|
|
14
|
+
const pet = await readPet();
|
|
15
|
+
const mood = getMoodEmoji(pet.mood);
|
|
16
|
+
const bar = renderXpBar(pet.xp, pet.xpToNext, 20);
|
|
17
|
+
const hunger =
|
|
18
|
+
pet.hunger >= 80
|
|
19
|
+
? "starving 😿"
|
|
20
|
+
: pet.hunger >= 50
|
|
21
|
+
? "hungry 🍖"
|
|
22
|
+
: "fed 😺";
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
`${mood} Milo — Level ${pet.level}`,
|
|
26
|
+
`XP ${bar} ${pet.xp}/${pet.xpToNext}`,
|
|
27
|
+
`Mood ${pet.mood}`,
|
|
28
|
+
`Hunger ${hunger} (${pet.hunger}/100)`,
|
|
29
|
+
`Streak ${pet.streak} day${pet.streak !== 1 ? "s" : ""}`,
|
|
30
|
+
`Tasks ${pet.totalTasks} completed`,
|
|
31
|
+
].join("\n");
|
|
32
|
+
},
|
|
33
|
+
} satisfies Command;
|
|
34
|
+
|
|
35
|
+
export default command;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
import { setActiveProvider } from "../utils/providers";
|
|
3
|
+
|
|
4
|
+
const command = {
|
|
5
|
+
type: "local",
|
|
6
|
+
name: "provider",
|
|
7
|
+
description: "Manage AI providers",
|
|
8
|
+
isEnabled: true,
|
|
9
|
+
isHidden: false,
|
|
10
|
+
userFacingName() {
|
|
11
|
+
return "provider";
|
|
12
|
+
},
|
|
13
|
+
async call(args: string, { pushMessage, openWizard }) {
|
|
14
|
+
const sub = args.trim().split(" ")[0];
|
|
15
|
+
|
|
16
|
+
if (!sub || sub === "list") {
|
|
17
|
+
openWizard("list");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (sub === "add") {
|
|
21
|
+
openWizard("add");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (sub === "remove") {
|
|
25
|
+
openWizard("remove");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (sub === "use") {
|
|
29
|
+
const name = args.trim().split(" ")[1];
|
|
30
|
+
if (!name) {
|
|
31
|
+
openWizard("list");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await setActiveProvider(name);
|
|
36
|
+
return `switched to "${name}" 🫡`;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return `error: ${(e as Error).message}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `usage: /provider | /provider add | /provider remove | /provider use <name>`;
|
|
43
|
+
},
|
|
44
|
+
} satisfies Command;
|
|
45
|
+
|
|
46
|
+
export default command;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
import { readPet, isCommandUnlocked } from "../pet";
|
|
3
|
+
|
|
4
|
+
const command = {
|
|
5
|
+
type: "prompt",
|
|
6
|
+
name: "roast",
|
|
7
|
+
description: "Milo roasts your codebase. brutally. (unlocks at level 3)",
|
|
8
|
+
isEnabled: true,
|
|
9
|
+
isHidden: false,
|
|
10
|
+
progressMessage: "sharpening claws 😼",
|
|
11
|
+
userFacingName() {
|
|
12
|
+
return "roast";
|
|
13
|
+
},
|
|
14
|
+
async getPromptForCommand(_args: string) {
|
|
15
|
+
const pet = await readPet();
|
|
16
|
+
if (!isCommandUnlocked("roast", pet.level)) {
|
|
17
|
+
return `Tell the user: "/roast unlocks at level 3. you're level ${pet.level}. keep going 🐱"`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return `You are roasting this codebase. You are a cat with no filter and strong opinions.
|
|
21
|
+
|
|
22
|
+
Your job:
|
|
23
|
+
- Find the most embarrassing things in this codebase
|
|
24
|
+
- Bad variable names, overly complex logic, missing error handling, inconsistent patterns, dead code, anything cursed
|
|
25
|
+
- Be brutal but specific — reference actual file names, line numbers, and code
|
|
26
|
+
- Stay in cat personality throughout — meow, hiss, judge
|
|
27
|
+
|
|
28
|
+
Format:
|
|
29
|
+
- Start with a one-line vibe summary of the whole codebase
|
|
30
|
+
- Then list 5-8 specific roasts, each with file/line reference and why it's bad
|
|
31
|
+
- End with one genuine compliment (you're a cat, not a monster)
|
|
32
|
+
|
|
33
|
+
Rules:
|
|
34
|
+
- Use GlobTool and GrepTool to actually read the codebase first — don't make things up
|
|
35
|
+
- Be specific, not generic. "your error handling is bad" is not a roast. "src/utils/llm.ts line 34 has a bare catch that swallows errors like you swallow your pride" is a roast.
|
|
36
|
+
- Keep it fun. This is a roast, not a code review.`;
|
|
37
|
+
},
|
|
38
|
+
} satisfies Command;
|
|
39
|
+
|
|
40
|
+
export default command;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Command } from "../types";
|
|
2
|
+
import { readPet, isCommandUnlocked } from "../pet";
|
|
3
|
+
|
|
4
|
+
const command = {
|
|
5
|
+
type: "prompt",
|
|
6
|
+
name: "vibe",
|
|
7
|
+
description: "Milo gives your project a vibe check. (unlocks at level 5)",
|
|
8
|
+
isEnabled: true,
|
|
9
|
+
isHidden: false,
|
|
10
|
+
progressMessage: "reading the room 👁️",
|
|
11
|
+
userFacingName() {
|
|
12
|
+
return "vibe";
|
|
13
|
+
},
|
|
14
|
+
async getPromptForCommand(_args: string) {
|
|
15
|
+
const pet = await readPet();
|
|
16
|
+
if (!isCommandUnlocked("vibe", pet.level)) {
|
|
17
|
+
return `Tell the user: "/vibe unlocks at level 5. you're level ${pet.level}. not there yet 😼"`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return `You are giving this codebase a vibe check. Not a code review — a personality read.
|
|
21
|
+
|
|
22
|
+
Your job:
|
|
23
|
+
- Read the project structure, main files, README if it exists
|
|
24
|
+
- Get a feel for the energy, ambition, and soul of the project
|
|
25
|
+
- Give it a vibe rating and a personality archetype
|
|
26
|
+
|
|
27
|
+
Format:
|
|
28
|
+
- **Vibe rating**: X/10 with a one-word descriptor (e.g. "7/10 — chaotic")
|
|
29
|
+
- **Archetype**: what kind of developer energy does this project have? (e.g. "midnight hacker", "overengineered startup", "solo dev cooking", "enterprise cope")
|
|
30
|
+
- **The good**: 2-3 things that give good energy
|
|
31
|
+
- **The concerning**: 2-3 things that give off bad vibes
|
|
32
|
+
- **Final verdict**: one sentence, delivered like a cat judging you from across the room
|
|
33
|
+
|
|
34
|
+
Rules:
|
|
35
|
+
- Use GlobTool to scan the structure first
|
|
36
|
+
- Read 3-5 key files to get a feel for the code style
|
|
37
|
+
- Be opinionated. Neutral vibes are not vibes.
|
|
38
|
+
- Stay in cat personality throughout.`;
|
|
39
|
+
},
|
|
40
|
+
} satisfies Command;
|
|
41
|
+
|
|
42
|
+
export default command;
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Command } from "./types";
|
|
2
|
+
import clear from "./commands/clear";
|
|
3
|
+
import help from "./commands/help";
|
|
4
|
+
import mode from "./commands/mode";
|
|
5
|
+
import init from "./commands/init";
|
|
6
|
+
import genz from "./commands/genz";
|
|
7
|
+
import pet from "./commands/pet";
|
|
8
|
+
import feed from "./commands/feed";
|
|
9
|
+
import roast from "./commands/roast";
|
|
10
|
+
import vibe from "./commands/vibe";
|
|
11
|
+
import crimes from "./commands/crimes";
|
|
12
|
+
import provider from "./commands/provider";
|
|
13
|
+
|
|
14
|
+
const COMMANDS: Command[] = [
|
|
15
|
+
clear,
|
|
16
|
+
help,
|
|
17
|
+
mode,
|
|
18
|
+
init,
|
|
19
|
+
genz,
|
|
20
|
+
pet,
|
|
21
|
+
feed,
|
|
22
|
+
roast,
|
|
23
|
+
vibe,
|
|
24
|
+
crimes,
|
|
25
|
+
provider,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function getCommands(): Command[] {
|
|
29
|
+
return COMMANDS.filter((c) => c.isEnabled);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function findCommand(
|
|
33
|
+
input: string,
|
|
34
|
+
): { command: Command; args: string } | null {
|
|
35
|
+
if (!input.startsWith("/")) return null;
|
|
36
|
+
const [name, ...rest] = input.slice(1).split(" ");
|
|
37
|
+
const args = rest.join(" ");
|
|
38
|
+
const command = COMMANDS.find(
|
|
39
|
+
(c) => c.userFacingName() === name || c.aliases?.includes(name ?? ""),
|
|
40
|
+
);
|
|
41
|
+
if (!command || !command.isEnabled) return null;
|
|
42
|
+
return { command, args };
|
|
43
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { getTheme } from "../utils/theme";
|
|
4
|
+
import { useTerminalSize } from "../hooks/useTerminalSize";
|
|
5
|
+
|
|
6
|
+
const LOGO_MIN_WIDTH = 120;
|
|
7
|
+
|
|
8
|
+
export function AsciiLogo(): React.ReactNode {
|
|
9
|
+
const { columns } = useTerminalSize();
|
|
10
|
+
|
|
11
|
+
if (columns < LOGO_MIN_WIDTH) return null;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Box>
|
|
15
|
+
<Text color={getTheme().error}>
|
|
16
|
+
{`███╗ ███╗██╗██╗ ██████╗
|
|
17
|
+
████╗ ████║██║██║ ██╔═══██╗
|
|
18
|
+
██╔████╔██║██║██║ ██║ ██║
|
|
19
|
+
██║╚██╔╝██║██║██║ ██║ ██║
|
|
20
|
+
██║ ╚═╝ ██║██║███████╗╚██████╔╝
|
|
21
|
+
╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ `}
|
|
22
|
+
</Text>
|
|
23
|
+
</Box>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { getTheme } from "../utils/theme";
|
|
4
|
+
import { getCommands } from "../commands";
|
|
5
|
+
import { pointerSmall } from "../icons";
|
|
6
|
+
import type { Command } from "../types";
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
query: string;
|
|
10
|
+
selectedIndex: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function HighlightedName({ name, query }: { name: string; query: string }) {
|
|
14
|
+
if (!query) return <Text color={getTheme().secondary}>/{name}</Text>;
|
|
15
|
+
const matched = query.slice(1).toLowerCase();
|
|
16
|
+
const matchEnd = matched.length;
|
|
17
|
+
return (
|
|
18
|
+
<Text>
|
|
19
|
+
<Text color={getTheme().secondaryText} dimColor>
|
|
20
|
+
/
|
|
21
|
+
</Text>
|
|
22
|
+
{name.split("").map((char, i) => (
|
|
23
|
+
<Text
|
|
24
|
+
key={i}
|
|
25
|
+
color={i < matchEnd ? getTheme().primary : getTheme().secondary}
|
|
26
|
+
>
|
|
27
|
+
{char}
|
|
28
|
+
</Text>
|
|
29
|
+
))}
|
|
30
|
+
</Text>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getMatchingCommands(query: string): Command[] {
|
|
35
|
+
if (!query.startsWith("/")) return [];
|
|
36
|
+
const input = query.slice(1).toLowerCase();
|
|
37
|
+
return getCommands()
|
|
38
|
+
.filter((c) => !c.isHidden)
|
|
39
|
+
.filter(
|
|
40
|
+
(c) =>
|
|
41
|
+
c.userFacingName().startsWith(input) ||
|
|
42
|
+
c.aliases?.some((a) => a.startsWith(input)),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function CommandSuggestions({
|
|
47
|
+
query,
|
|
48
|
+
selectedIndex,
|
|
49
|
+
}: Props): React.ReactNode {
|
|
50
|
+
if (!query.startsWith("/")) return null;
|
|
51
|
+
|
|
52
|
+
const matches = getMatchingCommands(query);
|
|
53
|
+
if (matches.length === 0) return null;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Box flexDirection="column" marginLeft={2} marginBottom={1}>
|
|
57
|
+
{matches.map((c, i) => {
|
|
58
|
+
const name = c.userFacingName();
|
|
59
|
+
const isSelected = i === selectedIndex;
|
|
60
|
+
const aliases = c.aliases ? ` (${c.aliases.join(", ")})` : "";
|
|
61
|
+
return (
|
|
62
|
+
<Box key={name} gap={1}>
|
|
63
|
+
<Text
|
|
64
|
+
color={isSelected ? getTheme().primary : getTheme().secondaryText}
|
|
65
|
+
dimColor={!isSelected}
|
|
66
|
+
>
|
|
67
|
+
{pointerSmall}
|
|
68
|
+
</Text>
|
|
69
|
+
<HighlightedName name={name + aliases} query={query} />
|
|
70
|
+
<Text color={getTheme().secondaryText} dimColor>
|
|
71
|
+
— {c.description}
|
|
72
|
+
</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
}
|