@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
@@ -0,0 +1,152 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { getTheme } from "../../utils/theme";
4
+ import { useTerminalSize } from "../../hooks/useTerminalSize";
5
+ import { StructuredDiff } from "../StructuredDiff";
6
+ import { parsePatch } from "diff";
7
+ import type { PermissionDecision } from "../../permissions";
8
+ import type { PermissionRequest } from "../../types";
9
+ import {
10
+ arrowRight,
11
+ cornerBottomLeft,
12
+ cornerTopLeft,
13
+ line,
14
+ upDownArrow,
15
+ } from "../../icons";
16
+
17
+ type Props = {
18
+ permission: PermissionRequest;
19
+ onDecide: (decision: PermissionDecision) => void;
20
+ };
21
+
22
+ const OPTIONS: { label: string; value: PermissionDecision }[] = [
23
+ { label: "Yes", value: "allow" },
24
+ {
25
+ label: "Yes, and don't ask again in this session.",
26
+ value: "allow_session",
27
+ },
28
+ { label: "No (esc)", value: "deny" },
29
+ ];
30
+
31
+ function getBashPreview(input: unknown): string {
32
+ if (!input || typeof input !== "object") return "";
33
+ return String((input as any).command ?? "");
34
+ }
35
+
36
+ function getFilePreview(toolName: string, input: unknown) {
37
+ if (!input || typeof input !== "object") return null;
38
+ const a = input as Record<string, unknown>;
39
+
40
+ if (toolName === "FileWriteTool") {
41
+ const content = String(a.content ?? "");
42
+ if (!content) return null;
43
+ const lines = content.split("\n").map((l) => "+" + l);
44
+ return {
45
+ oldStart: 1,
46
+ oldLines: 0,
47
+ newStart: 1,
48
+ newLines: lines.length,
49
+ lines,
50
+ };
51
+ }
52
+
53
+ if (toolName === "FileEditTool") {
54
+ const patch = String(a.patch ?? "");
55
+ if (!patch) return null;
56
+ const parsed = parsePatch(patch);
57
+ return parsed[0]?.hunks[0] ?? null;
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ export function PermissionCard({
64
+ permission,
65
+ onDecide,
66
+ }: Props): React.ReactNode {
67
+ const { columns } = useTerminalSize();
68
+ const theme = getTheme();
69
+ const [selectedIndex, setSelectedIndex] = useState(0);
70
+ const { toolName, input } = permission;
71
+ const isBash = toolName === "BashTool";
72
+ const isFile = toolName === "FileWriteTool" || toolName === "FileEditTool";
73
+ const hunk = isFile ? getFilePreview(toolName, input) : null;
74
+ const command = isBash ? getBashPreview(input) : null;
75
+ const path = isFile ? String((input as any).path ?? "") : null;
76
+
77
+ useInput((_, key) => {
78
+ if (key.upArrow) {
79
+ setSelectedIndex((i) => Math.max(0, i - 1));
80
+ }
81
+ if (key.downArrow) {
82
+ setSelectedIndex((i) => Math.min(OPTIONS.length - 1, i + 1));
83
+ }
84
+ if (key.return) {
85
+ onDecide(OPTIONS[selectedIndex]!.value);
86
+ }
87
+ if (key.escape) {
88
+ onDecide("deny");
89
+ }
90
+ });
91
+
92
+ return (
93
+ <Box flexDirection="column" marginTop={1} paddingX={1} width={columns}>
94
+ <Box marginBottom={1}>
95
+ <Text color={theme.primary} bold>
96
+ {isBash ? "Run command" : "Edit file"}
97
+ </Text>
98
+ {path && <Text color={theme.secondaryText}> · {path}</Text>}
99
+ </Box>
100
+
101
+ {isBash && command && (
102
+ <Box flexDirection="column" marginBottom={1}>
103
+ <Box flexDirection="row">
104
+ <Text color={theme.secondaryText} dimColor>
105
+ {cornerBottomLeft}
106
+ {line}{" "}
107
+ </Text>
108
+ <Text color={theme.secondary}>$ {command}</Text>
109
+ </Box>
110
+ </Box>
111
+ )}
112
+
113
+ {hunk && (
114
+ <Box marginBottom={1} flexDirection="column">
115
+ <StructuredDiff patch={hunk} dim={false} width={columns - 8} />
116
+ </Box>
117
+ )}
118
+
119
+ <Box flexDirection="column" marginTop={1}>
120
+ {OPTIONS.map((opt, i) => {
121
+ const selected = i === selectedIndex;
122
+ const color =
123
+ opt.value === "deny"
124
+ ? theme.error
125
+ : opt.value === "allow_session"
126
+ ? theme.warning
127
+ : theme.success;
128
+ return (
129
+ <Box key={opt.value} flexDirection="row" gap={1}>
130
+ <Text color={selected ? color : theme.secondaryText}>
131
+ {selected ? arrowRight : " "}
132
+ </Text>
133
+ <Text>{i + 1}.</Text>
134
+ <Text
135
+ color={selected ? color : theme.secondaryText}
136
+ bold={selected}
137
+ >
138
+ {opt.label}
139
+ </Text>
140
+ </Box>
141
+ );
142
+ })}
143
+ </Box>
144
+
145
+ <Box marginTop={1}>
146
+ <Text color={theme.secondaryText} dimColor>
147
+ {upDownArrow} to select · enter to confirm · esc to deny
148
+ </Text>
149
+ </Box>
150
+ </Box>
151
+ );
152
+ }
package/src/history.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { readFile, writeFile, mkdir } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ const HISTORY_FILE = join(homedir(), ".milo", "history.json");
7
+ const MAX_HISTORY = 100;
8
+
9
+ export async function getHistory(): Promise<string[]> {
10
+ try {
11
+ const raw = await readFile(HISTORY_FILE, "utf-8");
12
+ return JSON.parse(raw);
13
+ } catch {
14
+ return [];
15
+ }
16
+ }
17
+
18
+ export async function addToHistory(input: string): Promise<void> {
19
+ const history = await getHistory();
20
+ if (history[0] === input) return;
21
+ history.unshift(input);
22
+ await mkdir(dirname(HISTORY_FILE), { recursive: true });
23
+ await writeFile(
24
+ HISTORY_FILE,
25
+ JSON.stringify(history.slice(0, MAX_HISTORY), null, 2),
26
+ );
27
+ }
File without changes
@@ -0,0 +1,271 @@
1
+ import { useState, useCallback, useRef, useEffect } from "react";
2
+ import { chatWithModel } from "../utils/chat";
3
+ import { createAgent } from "../utils/agent";
4
+ import { planWithModel } from "../utils/plan";
5
+ import { findCommand } from "../commands";
6
+ import { onPermissionRequest, resolvePermission } from "../permissions";
7
+ import { awardXP, getLevelFlavor, LEVEL_UP_MESSAGES } from "../pet";
8
+ import type {
9
+ Mode,
10
+ ChatMessage,
11
+ OrchestratorEvent,
12
+ PermissionRequest,
13
+ } from "../types";
14
+ import type { Session } from "../utils/session";
15
+ import type { PermissionDecision } from "../permissions";
16
+ import { info, radioOn, star } from "../icons";
17
+
18
+ export type WizardMode = "add" | "edit" | "remove" | "list";
19
+
20
+ export function useChat(initialMode: Mode = "agent") {
21
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
22
+ const [session, setSession] = useState<Session | undefined>(undefined);
23
+ const [loading, setLoading] = useState(false);
24
+ const [mode, _setMode] = useState<Mode>(initialMode);
25
+ const [pendingPermission, setPendingPermission] =
26
+ useState<PermissionRequest | null>(null);
27
+ const [pendingWizard, setPendingWizard] = useState<WizardMode | null>(null);
28
+ const modeRef = useRef<Mode>(initialMode);
29
+ const abortControllerRef = useRef<AbortController>(new AbortController());
30
+
31
+ const setMode = useCallback((m: Mode) => {
32
+ modeRef.current = m;
33
+ _setMode(m);
34
+ }, []);
35
+
36
+ const clearMessages = useCallback(() => {
37
+ setMessages([]);
38
+ setSession(undefined);
39
+ }, []);
40
+
41
+ const pushMessage = useCallback((text: string) => {
42
+ setMessages((prev) => [
43
+ ...prev,
44
+ { id: crypto.randomUUID(), type: "assistant", text },
45
+ ]);
46
+ }, []);
47
+
48
+ useEffect(() => {
49
+ const unsub = onPermissionRequest((p) => {
50
+ setPendingPermission({ toolName: p.toolName, input: p.input });
51
+ });
52
+ return unsub;
53
+ }, []);
54
+
55
+ const decide = useCallback((decision: PermissionDecision) => {
56
+ resolvePermission(decision);
57
+ setPendingPermission(null);
58
+ }, []);
59
+
60
+ const closeWizard = useCallback(
61
+ (message?: string) => {
62
+ setPendingWizard(null);
63
+ if (message) pushMessage(message);
64
+ },
65
+ [pushMessage],
66
+ );
67
+
68
+ const handleOrchestratorEvent = useCallback(
69
+ (event: OrchestratorEvent) => {
70
+ switch (event.type) {
71
+ case "plan_created":
72
+ pushMessage(
73
+ `${info} Plan created — ${event.tasks.length} subtasks:\n${event.tasks
74
+ .map((t) => ` [${t.id}] ${t.subtask}`)
75
+ .join("\n")}`,
76
+ );
77
+ break;
78
+ case "agent_start":
79
+ setMessages((prev) => [
80
+ ...prev,
81
+ {
82
+ id: `agent-${event.taskId}`,
83
+ type: "tool_call",
84
+ toolName: "AgentTool",
85
+ input: { task: event.subtask },
86
+ },
87
+ ]);
88
+ break;
89
+ case "agent_done":
90
+ setMessages((prev) =>
91
+ prev.map((msg) =>
92
+ msg.id === `agent-${event.taskId}`
93
+ ? {
94
+ id: msg.id,
95
+ type: "tool_result" as const,
96
+ toolName: "AgentTool",
97
+ input: msg.type === "tool_call" ? msg.input : undefined,
98
+ output: { output: event.result },
99
+ success: true,
100
+ }
101
+ : msg,
102
+ ),
103
+ );
104
+ break;
105
+ case "connecting":
106
+ pushMessage(`${radioOn} Connecting all agents...`);
107
+ break;
108
+ case "done":
109
+ pushMessage(`${star} Orchestration complete.`);
110
+ break;
111
+ }
112
+ },
113
+ [pushMessage],
114
+ );
115
+
116
+ const submit = useCallback(
117
+ async (input: string) => {
118
+ if (!input.trim() || loading || pendingPermission || pendingWizard)
119
+ return;
120
+
121
+ setMessages((prev) => [
122
+ ...prev,
123
+ { id: crypto.randomUUID(), type: "user", text: input },
124
+ ]);
125
+ setLoading(true);
126
+
127
+ abortControllerRef.current = new AbortController();
128
+
129
+ const found = findCommand(input);
130
+ if (found) {
131
+ const { command, args } = found;
132
+ if (command.type === "local") {
133
+ const result = await command.call(args, {
134
+ clearMessages,
135
+ session,
136
+ setSession,
137
+ mode: modeRef.current,
138
+ setMode,
139
+ pushMessage,
140
+ abortController: abortControllerRef.current,
141
+ openWizard: setPendingWizard,
142
+ });
143
+ if (result) pushMessage(result);
144
+ setLoading(false);
145
+ return;
146
+ }
147
+ if (command.type === "prompt") {
148
+ input = await command.getPromptForCommand(args);
149
+ }
150
+ }
151
+
152
+ try {
153
+ const onToolCall = (toolCall: {
154
+ id: string;
155
+ toolName: string;
156
+ input: unknown;
157
+ }) => {
158
+ setMessages((prev) => [
159
+ ...prev,
160
+ {
161
+ id: toolCall.id,
162
+ type: "tool_call",
163
+ toolName: toolCall.toolName,
164
+ input: toolCall.input,
165
+ },
166
+ ]);
167
+ };
168
+
169
+ const onToolResult = (toolResult: {
170
+ id: string;
171
+ toolName: string;
172
+ output: unknown;
173
+ }) => {
174
+ awardXP(toolResult.toolName)
175
+ .then(({ pet, leveledUp }) => {
176
+ if (leveledUp) {
177
+ const flavor = getLevelFlavor(pet.level);
178
+ const unlock = LEVEL_UP_MESSAGES[pet.level];
179
+ const lines = [
180
+ `⚡ level up! milo is now level ${pet.level} 🐱`,
181
+ flavor,
182
+ ];
183
+ if (unlock) lines.push(unlock);
184
+ pushMessage(lines.join("\n"));
185
+ }
186
+ })
187
+ .catch(() => {});
188
+
189
+ setMessages((prev) =>
190
+ prev.map((msg) =>
191
+ msg.id === toolResult.id
192
+ ? {
193
+ id: toolResult.id,
194
+ type: "tool_result" as const,
195
+ toolName: toolResult.toolName,
196
+ input: msg.type === "tool_call" ? msg.input : undefined,
197
+ output: toolResult.output,
198
+ success: true,
199
+ }
200
+ : msg,
201
+ ),
202
+ );
203
+ };
204
+
205
+ const actualPrompt = input;
206
+ let text: string;
207
+ let newSession: Session;
208
+
209
+ const currentMode = modeRef.current;
210
+ const result =
211
+ currentMode === "plan"
212
+ ? await planWithModel(
213
+ actualPrompt,
214
+ session,
215
+ onToolCall,
216
+ onToolResult,
217
+ handleOrchestratorEvent,
218
+ )
219
+ : await (currentMode === "chat" ? chatWithModel : createAgent)(
220
+ actualPrompt,
221
+ session,
222
+ onToolCall,
223
+ onToolResult,
224
+ );
225
+ text = result.text;
226
+ newSession = result.session;
227
+
228
+ setMessages((prev) => [
229
+ ...prev,
230
+ { id: crypto.randomUUID(), type: "assistant", text },
231
+ ]);
232
+ setSession(newSession);
233
+ } catch (err) {
234
+ setMessages((prev) => [
235
+ ...prev,
236
+ {
237
+ id: crypto.randomUUID(),
238
+ type: "assistant",
239
+ text: `Error: ${(err as Error).message}`,
240
+ },
241
+ ]);
242
+ } finally {
243
+ setLoading(false);
244
+ }
245
+ },
246
+ [
247
+ loading,
248
+ pendingPermission,
249
+ pendingWizard,
250
+ session,
251
+ clearMessages,
252
+ setMode,
253
+ pushMessage,
254
+ handleOrchestratorEvent,
255
+ ],
256
+ );
257
+
258
+ return {
259
+ messages,
260
+ loading,
261
+ mode,
262
+ setMode,
263
+ submit,
264
+ clearMessages,
265
+ pendingPermission,
266
+ decide,
267
+ pushMessage,
268
+ pendingWizard,
269
+ closeWizard,
270
+ };
271
+ }
@@ -0,0 +1,35 @@
1
+ import { useRef } from "react";
2
+
3
+ export const DOUBLE_PRESS_TIMEOUT_MS = 2000;
4
+
5
+ export function useDoublePress(
6
+ setPending: (pending: boolean) => void,
7
+ onDoublePress: () => void,
8
+ onFirstPress?: () => void,
9
+ ): () => void {
10
+ const lastPressRef = useRef<number>(0);
11
+ const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
12
+
13
+ return () => {
14
+ const now = Date.now();
15
+ const timeSinceLastPress = now - lastPressRef.current;
16
+
17
+ if (timeSinceLastPress <= DOUBLE_PRESS_TIMEOUT_MS && timeoutRef.current) {
18
+ if (timeoutRef.current) {
19
+ clearTimeout(timeoutRef.current);
20
+ timeoutRef.current = undefined;
21
+ }
22
+ onDoublePress();
23
+ setPending(false);
24
+ } else {
25
+ onFirstPress?.();
26
+ setPending(true);
27
+ timeoutRef.current = setTimeout(
28
+ () => setPending(false),
29
+ DOUBLE_PRESS_TIMEOUT_MS,
30
+ );
31
+ }
32
+
33
+ lastPressRef.current = now;
34
+ };
35
+ }
@@ -0,0 +1,24 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export function useTerminalSize() {
4
+ const [size, setSize] = useState({
5
+ columns: process.stdout.columns || 80,
6
+ rows: process.stdout.rows || 24,
7
+ });
8
+
9
+ useEffect(() => {
10
+ function updateSize() {
11
+ setSize({
12
+ columns: process.stdout.columns || 80,
13
+ rows: process.stdout.rows || 24,
14
+ });
15
+ }
16
+
17
+ process.stdout.on("resize", updateSize);
18
+ return () => {
19
+ process.stdout.off("resize", updateSize);
20
+ };
21
+ }, []);
22
+
23
+ return size;
24
+ }