@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/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;
@@ -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
+ }