@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
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import type { Token } from "marked";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { EOL } from "os";
|
|
5
|
+
import { highlight, supportsLanguage } from "cli-highlight";
|
|
6
|
+
import { cornerTopLeft, cornerBottomLeft, lineVertical, line } from "../icons";
|
|
7
|
+
|
|
8
|
+
const STRIPPED_TAGS = [
|
|
9
|
+
"commit_analysis",
|
|
10
|
+
"context",
|
|
11
|
+
"function_analysis",
|
|
12
|
+
"pr_analysis",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function stripSystemMessages(content: string): string {
|
|
16
|
+
const regex = new RegExp(`<(${STRIPPED_TAGS.join("|")})>.*?</\\1>\n?`, "gs");
|
|
17
|
+
return content.replace(regex, "").trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function applyMarkdown(content: string): string {
|
|
21
|
+
return marked
|
|
22
|
+
.lexer(stripSystemMessages(content))
|
|
23
|
+
.map((_) => format(_))
|
|
24
|
+
.join("")
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatCodeBlock(text: string, lang?: string): string {
|
|
29
|
+
const highlighted =
|
|
30
|
+
lang && supportsLanguage(lang)
|
|
31
|
+
? highlight(text, { language: lang })
|
|
32
|
+
: highlight(text, { language: "markdown" });
|
|
33
|
+
|
|
34
|
+
const lines = highlighted.split("\n");
|
|
35
|
+
const top =
|
|
36
|
+
chalk.dim(cornerTopLeft + line) + (lang ? chalk.dim(` ${lang}`) : "");
|
|
37
|
+
const body = lines.map((l) => chalk.dim(lineVertical + " ") + l).join(EOL);
|
|
38
|
+
const bottom = chalk.dim(cornerBottomLeft + line);
|
|
39
|
+
|
|
40
|
+
return top + EOL + body + EOL + bottom + EOL;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function format(
|
|
44
|
+
token: Token,
|
|
45
|
+
listDepth = 0,
|
|
46
|
+
orderedListNumber: number | null = null,
|
|
47
|
+
parent: Token | null = null,
|
|
48
|
+
): string {
|
|
49
|
+
switch (token.type) {
|
|
50
|
+
case "blockquote":
|
|
51
|
+
return chalk.dim.italic(
|
|
52
|
+
(token.tokens ?? []).map((_) => format(_)).join(""),
|
|
53
|
+
);
|
|
54
|
+
case "code":
|
|
55
|
+
return formatCodeBlock(token.text, token.lang);
|
|
56
|
+
case "codespan":
|
|
57
|
+
return chalk.blue(token.text);
|
|
58
|
+
case "em":
|
|
59
|
+
return chalk.italic((token.tokens ?? []).map((_) => format(_)).join(""));
|
|
60
|
+
case "strong":
|
|
61
|
+
return chalk.bold((token.tokens ?? []).map((_) => format(_)).join(""));
|
|
62
|
+
case "heading":
|
|
63
|
+
switch (token.depth) {
|
|
64
|
+
case 1:
|
|
65
|
+
return (
|
|
66
|
+
chalk.bold.italic.underline(
|
|
67
|
+
(token.tokens ?? []).map((_) => format(_)).join(""),
|
|
68
|
+
) +
|
|
69
|
+
EOL +
|
|
70
|
+
EOL
|
|
71
|
+
);
|
|
72
|
+
case 2:
|
|
73
|
+
return (
|
|
74
|
+
chalk.bold((token.tokens ?? []).map((_) => format(_)).join("")) +
|
|
75
|
+
EOL +
|
|
76
|
+
EOL
|
|
77
|
+
);
|
|
78
|
+
default:
|
|
79
|
+
return (
|
|
80
|
+
chalk.bold.dim(
|
|
81
|
+
(token.tokens ?? []).map((_) => format(_)).join(""),
|
|
82
|
+
) +
|
|
83
|
+
EOL +
|
|
84
|
+
EOL
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
case "hr":
|
|
88
|
+
return "---";
|
|
89
|
+
case "image":
|
|
90
|
+
return `[Image: ${token.title}: ${token.href}]`;
|
|
91
|
+
case "link":
|
|
92
|
+
return chalk.blue(token.href);
|
|
93
|
+
case "list": {
|
|
94
|
+
return token.items
|
|
95
|
+
.map((_: Token, index: number) =>
|
|
96
|
+
format(
|
|
97
|
+
_,
|
|
98
|
+
listDepth,
|
|
99
|
+
token.ordered ? token.start + index : null,
|
|
100
|
+
token,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
.join("");
|
|
104
|
+
}
|
|
105
|
+
case "list_item":
|
|
106
|
+
return (token.tokens ?? [])
|
|
107
|
+
.map(
|
|
108
|
+
(_) =>
|
|
109
|
+
`${" ".repeat(listDepth)}${format(_, listDepth + 1, orderedListNumber, token)}`,
|
|
110
|
+
)
|
|
111
|
+
.join("");
|
|
112
|
+
case "paragraph":
|
|
113
|
+
return (token.tokens ?? []).map((_) => format(_)).join("") + EOL;
|
|
114
|
+
case "space":
|
|
115
|
+
return EOL;
|
|
116
|
+
case "text":
|
|
117
|
+
if (parent?.type === "list_item") {
|
|
118
|
+
return `${orderedListNumber === null ? "-" : getListNumber(listDepth, orderedListNumber) + "."} ${token.tokens ? token.tokens.map((_) => format(_, listDepth, orderedListNumber, token)).join("") : token.text}${EOL}`;
|
|
119
|
+
} else {
|
|
120
|
+
return token.text;
|
|
121
|
+
}
|
|
122
|
+
case "table": {
|
|
123
|
+
const headers = (token.header as any[]).map((h: any) =>
|
|
124
|
+
((h.tokens ?? []) as Token[]).map((_) => format(_)).join(""),
|
|
125
|
+
);
|
|
126
|
+
const rows = (token.rows as any[][]).map((row: any[]) =>
|
|
127
|
+
row.map((cell: any) =>
|
|
128
|
+
((cell.tokens ?? []) as Token[]).map((_) => format(_)).join(""),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const colWidths = headers.map((h: string, i: number) =>
|
|
133
|
+
Math.max(h.length, ...rows.map((r: string[]) => (r[i] ?? "").length)),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const formatRow = (cells: string[]) =>
|
|
137
|
+
cells.map((c, i) => c.padEnd(colWidths[i] ?? 0)).join(" ");
|
|
138
|
+
|
|
139
|
+
const header = chalk.bold(formatRow(headers));
|
|
140
|
+
const separator = colWidths.map((w: number) => "─".repeat(w)).join(" ");
|
|
141
|
+
const body = rows.map((r: string[]) => formatRow(r)).join(EOL);
|
|
142
|
+
|
|
143
|
+
return header + EOL + chalk.dim(separator) + EOL + body + EOL;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const DEPTH_1_LIST_NUMBERS = [
|
|
150
|
+
"a",
|
|
151
|
+
"b",
|
|
152
|
+
"c",
|
|
153
|
+
"d",
|
|
154
|
+
"e",
|
|
155
|
+
"f",
|
|
156
|
+
"g",
|
|
157
|
+
"h",
|
|
158
|
+
"i",
|
|
159
|
+
"j",
|
|
160
|
+
"k",
|
|
161
|
+
"l",
|
|
162
|
+
"m",
|
|
163
|
+
"n",
|
|
164
|
+
"o",
|
|
165
|
+
"p",
|
|
166
|
+
"q",
|
|
167
|
+
"r",
|
|
168
|
+
"s",
|
|
169
|
+
"t",
|
|
170
|
+
"u",
|
|
171
|
+
"v",
|
|
172
|
+
"w",
|
|
173
|
+
"x",
|
|
174
|
+
"y",
|
|
175
|
+
"z",
|
|
176
|
+
"aa",
|
|
177
|
+
"ab",
|
|
178
|
+
"ac",
|
|
179
|
+
"ad",
|
|
180
|
+
"ae",
|
|
181
|
+
"af",
|
|
182
|
+
"ag",
|
|
183
|
+
"ah",
|
|
184
|
+
"ai",
|
|
185
|
+
"aj",
|
|
186
|
+
"ak",
|
|
187
|
+
"al",
|
|
188
|
+
"am",
|
|
189
|
+
"an",
|
|
190
|
+
"ao",
|
|
191
|
+
"ap",
|
|
192
|
+
"aq",
|
|
193
|
+
"ar",
|
|
194
|
+
"as",
|
|
195
|
+
"at",
|
|
196
|
+
"au",
|
|
197
|
+
"av",
|
|
198
|
+
"aw",
|
|
199
|
+
"ax",
|
|
200
|
+
"ay",
|
|
201
|
+
"az",
|
|
202
|
+
];
|
|
203
|
+
const DEPTH_2_LIST_NUMBERS = [
|
|
204
|
+
"i",
|
|
205
|
+
"ii",
|
|
206
|
+
"iii",
|
|
207
|
+
"iv",
|
|
208
|
+
"v",
|
|
209
|
+
"vi",
|
|
210
|
+
"vii",
|
|
211
|
+
"viii",
|
|
212
|
+
"ix",
|
|
213
|
+
"x",
|
|
214
|
+
"xi",
|
|
215
|
+
"xii",
|
|
216
|
+
"xiii",
|
|
217
|
+
"xiv",
|
|
218
|
+
"xv",
|
|
219
|
+
"xvi",
|
|
220
|
+
"xvii",
|
|
221
|
+
"xviii",
|
|
222
|
+
"xix",
|
|
223
|
+
"xx",
|
|
224
|
+
"xxi",
|
|
225
|
+
"xxii",
|
|
226
|
+
"xxiii",
|
|
227
|
+
"xxiv",
|
|
228
|
+
"xxv",
|
|
229
|
+
"xxvi",
|
|
230
|
+
"xxvii",
|
|
231
|
+
"xxviii",
|
|
232
|
+
"xxix",
|
|
233
|
+
"xxx",
|
|
234
|
+
"xxxi",
|
|
235
|
+
"xxxii",
|
|
236
|
+
"xxxiii",
|
|
237
|
+
"xxxiv",
|
|
238
|
+
"xxxv",
|
|
239
|
+
"xxxvi",
|
|
240
|
+
"xxxvii",
|
|
241
|
+
"xxxviii",
|
|
242
|
+
"xxxix",
|
|
243
|
+
"xl",
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
function getListNumber(listDepth: number, orderedListNumber: number): string {
|
|
247
|
+
switch (listDepth) {
|
|
248
|
+
case 0:
|
|
249
|
+
case 1:
|
|
250
|
+
return orderedListNumber.toString();
|
|
251
|
+
case 2:
|
|
252
|
+
return DEPTH_1_LIST_NUMBERS[orderedListNumber - 1]!;
|
|
253
|
+
case 3:
|
|
254
|
+
return DEPTH_2_LIST_NUMBERS[orderedListNumber - 1]!;
|
|
255
|
+
default:
|
|
256
|
+
return orderedListNumber.toString();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const INTERRUPT_MESSAGE = "[Request interrupted by user]";
|
|
2
|
+
export const INTERRUPT_MESSAGE_FOR_TOOL_USE =
|
|
3
|
+
"[Request interrupted by user for tool use]";
|
|
4
|
+
export const CANCEL_MESSAGE =
|
|
5
|
+
"The user doesn't want to take this action right now. STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
6
|
+
export const REJECT_MESSAGE =
|
|
7
|
+
"The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
8
|
+
export const NO_RESPONSE_REQUESTED = "No response requested.";
|
|
9
|
+
|
|
10
|
+
export const SYNTHETIC_ASSISTANT_MESSAGES = new Set([
|
|
11
|
+
INTERRUPT_MESSAGE,
|
|
12
|
+
INTERRUPT_MESSAGE_FOR_TOOL_USE,
|
|
13
|
+
CANCEL_MESSAGE,
|
|
14
|
+
REJECT_MESSAGE,
|
|
15
|
+
NO_RESPONSE_REQUESTED,
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function extractTag(html: string, tagName: string): string | null {
|
|
19
|
+
if (!html.trim() || !tagName.trim()) return null;
|
|
20
|
+
|
|
21
|
+
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22
|
+
const pattern = new RegExp(
|
|
23
|
+
`<${escapedTag}(?:\\s+[^>]*)?>([\\s\\S]*?)<\\/${escapedTag}>`,
|
|
24
|
+
"gi",
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
let match;
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let lastIndex = 0;
|
|
30
|
+
const openingTag = new RegExp(`<${escapedTag}(?:\\s+[^>]*?)?>`, "gi");
|
|
31
|
+
const closingTag = new RegExp(`<\\/${escapedTag}>`, "gi");
|
|
32
|
+
|
|
33
|
+
while ((match = pattern.exec(html)) !== null) {
|
|
34
|
+
const content = match[1];
|
|
35
|
+
const beforeMatch = html.slice(lastIndex, match.index);
|
|
36
|
+
|
|
37
|
+
depth = 0;
|
|
38
|
+
|
|
39
|
+
openingTag.lastIndex = 0;
|
|
40
|
+
while (openingTag.exec(beforeMatch) !== null) depth++;
|
|
41
|
+
|
|
42
|
+
closingTag.lastIndex = 0;
|
|
43
|
+
while (closingTag.exec(beforeMatch) !== null) depth--;
|
|
44
|
+
|
|
45
|
+
if (depth === 0 && content) return content;
|
|
46
|
+
|
|
47
|
+
lastIndex = match.index + match[0].length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const STRIPPED_TAGS = [
|
|
54
|
+
"commit_analysis",
|
|
55
|
+
"context",
|
|
56
|
+
"function_analysis",
|
|
57
|
+
"pr_analysis",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export function stripSystemMessages(content: string): string {
|
|
61
|
+
const regex = new RegExp(`<(${STRIPPED_TAGS.join("|")})>.*?</\\1>\n?`, "gs");
|
|
62
|
+
return content.replace(regex, "").trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isEmptyMessageText(text: string): boolean {
|
|
66
|
+
return (
|
|
67
|
+
stripSystemMessages(text).trim() === "" ||
|
|
68
|
+
text.trim() === INTERRUPT_MESSAGE_FOR_TOOL_USE
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getLastAssistantMessage(
|
|
73
|
+
messages: { role: string; content: string }[],
|
|
74
|
+
): string | undefined {
|
|
75
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
76
|
+
if (messages[i]?.role === "assistant") {
|
|
77
|
+
return messages[i]?.content;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getActiveProvider, buildProvider } from "./providers";
|
|
2
|
+
|
|
3
|
+
export async function getModel() {
|
|
4
|
+
const config = await getActiveProvider();
|
|
5
|
+
if (!config) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"no provider configured — run /provider add to get started 🐱",
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
model: buildProvider(config),
|
|
12
|
+
modelId: `${config.name} · ${config.model}`,
|
|
13
|
+
config,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { runLLM } from "./llm";
|
|
2
|
+
import type {
|
|
3
|
+
OnOrchestratorEvent,
|
|
4
|
+
StepToolCall,
|
|
5
|
+
StepToolResult,
|
|
6
|
+
} from "../types";
|
|
7
|
+
import { getPlanSystemPrompt } from "./systemPrompt";
|
|
8
|
+
import { createPlanTools } from "./tools";
|
|
9
|
+
import type { Session } from "./session";
|
|
10
|
+
|
|
11
|
+
export async function planWithModel(
|
|
12
|
+
prompt: string,
|
|
13
|
+
session?: Session,
|
|
14
|
+
onToolCall?: (t: StepToolCall) => void,
|
|
15
|
+
onToolResult?: (t: StepToolResult) => void,
|
|
16
|
+
onOrchestratorEvent?: OnOrchestratorEvent,
|
|
17
|
+
) {
|
|
18
|
+
return runLLM({
|
|
19
|
+
system: await getPlanSystemPrompt(),
|
|
20
|
+
prompt,
|
|
21
|
+
session,
|
|
22
|
+
tools: createPlanTools(onOrchestratorEvent),
|
|
23
|
+
onToolCall,
|
|
24
|
+
onToolResult,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { createGroq } from "@ai-sdk/groq";
|
|
5
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
6
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
7
|
+
import { createOllama } from "ai-sdk-ollama";
|
|
8
|
+
|
|
9
|
+
export type ProviderType = "groq" | "openai" | "anthropic" | "ollama";
|
|
10
|
+
|
|
11
|
+
export type ProviderConfig = {
|
|
12
|
+
name: string;
|
|
13
|
+
provider: ProviderType;
|
|
14
|
+
model: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
baseURL?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ProvidersFile = {
|
|
20
|
+
active: string;
|
|
21
|
+
providers: ProviderConfig[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PROVIDERS_FILE = join(homedir(), ".milo", "providers.json");
|
|
25
|
+
|
|
26
|
+
const EMPTY_PROVIDERS: ProvidersFile = {
|
|
27
|
+
active: "",
|
|
28
|
+
providers: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function readProviders(): Promise<ProvidersFile> {
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(PROVIDERS_FILE, "utf-8");
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
return { ...EMPTY_PROVIDERS };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function writeProviders(data: ProvidersFile): Promise<void> {
|
|
41
|
+
await mkdir(join(homedir(), ".milo"), { recursive: true });
|
|
42
|
+
await writeFile(PROVIDERS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getActiveProvider(): Promise<ProviderConfig | null> {
|
|
46
|
+
const data = await readProviders();
|
|
47
|
+
if (!data.active || data.providers.length === 0) return null;
|
|
48
|
+
return data.providers.find((p) => p.name === data.active) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function setActiveProvider(name: string): Promise<void> {
|
|
52
|
+
const data = await readProviders();
|
|
53
|
+
if (!data.providers.find((p) => p.name === name)) {
|
|
54
|
+
throw new Error(`Provider "${name}" not found`);
|
|
55
|
+
}
|
|
56
|
+
data.active = name;
|
|
57
|
+
await writeProviders(data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function addProvider(config: ProviderConfig): Promise<void> {
|
|
61
|
+
const data = await readProviders();
|
|
62
|
+
const exists = data.providers.findIndex((p) => p.name === config.name);
|
|
63
|
+
if (exists >= 0) {
|
|
64
|
+
data.providers[exists] = config;
|
|
65
|
+
} else {
|
|
66
|
+
data.providers.push(config);
|
|
67
|
+
}
|
|
68
|
+
// auto-set as active if it's the first one
|
|
69
|
+
if (data.providers.length === 1) {
|
|
70
|
+
data.active = config.name;
|
|
71
|
+
}
|
|
72
|
+
await writeProviders(data);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function removeProvider(name: string): Promise<void> {
|
|
76
|
+
const data = await readProviders();
|
|
77
|
+
data.providers = data.providers.filter((p) => p.name !== name);
|
|
78
|
+
if (data.active === name) {
|
|
79
|
+
data.active = data.providers[0]?.name ?? "";
|
|
80
|
+
}
|
|
81
|
+
await writeProviders(data);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildProvider(config: ProviderConfig) {
|
|
85
|
+
switch (config.provider) {
|
|
86
|
+
case "groq":
|
|
87
|
+
return createGroq({ apiKey: config.apiKey })(config.model);
|
|
88
|
+
case "openai":
|
|
89
|
+
return createOpenAI({
|
|
90
|
+
apiKey: config.apiKey,
|
|
91
|
+
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
92
|
+
})(config.model);
|
|
93
|
+
case "anthropic":
|
|
94
|
+
return createAnthropic({ apiKey: config.apiKey })(config.model);
|
|
95
|
+
case "ollama":
|
|
96
|
+
return createOllama({
|
|
97
|
+
baseURL: config.baseURL ?? "http://localhost:11434/api",
|
|
98
|
+
})(config.model);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { rgPath } from "@vscode/ripgrep";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
4
|
+
import { execFile } from "child_process";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
export type GrepMatch = { file: string; line: number; match: string };
|
|
10
|
+
|
|
11
|
+
const IGNORED_DIRS = new Set([
|
|
12
|
+
"node_modules",
|
|
13
|
+
".git",
|
|
14
|
+
"dist",
|
|
15
|
+
"build",
|
|
16
|
+
".next",
|
|
17
|
+
"out",
|
|
18
|
+
"coverage",
|
|
19
|
+
".turbo",
|
|
20
|
+
".cache",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
let _rgAvailable: boolean | null = null;
|
|
24
|
+
|
|
25
|
+
async function isRgAvailable(): Promise<boolean> {
|
|
26
|
+
if (_rgAvailable !== null) return _rgAvailable;
|
|
27
|
+
try {
|
|
28
|
+
await execFileAsync(rgPath, ["--version"]);
|
|
29
|
+
_rgAvailable = true;
|
|
30
|
+
} catch {
|
|
31
|
+
_rgAvailable = false;
|
|
32
|
+
}
|
|
33
|
+
return _rgAvailable;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function grepWithRg(
|
|
37
|
+
pattern: string,
|
|
38
|
+
path: string,
|
|
39
|
+
{
|
|
40
|
+
caseInsensitive = false,
|
|
41
|
+
include,
|
|
42
|
+
}: { caseInsensitive?: boolean; include?: string } = {},
|
|
43
|
+
): Promise<GrepMatch[]> {
|
|
44
|
+
const args = [
|
|
45
|
+
"--line-number",
|
|
46
|
+
"--no-heading",
|
|
47
|
+
"--color=never",
|
|
48
|
+
"--max-count=500",
|
|
49
|
+
"--glob=!node_modules/**",
|
|
50
|
+
"--glob=!.git/**",
|
|
51
|
+
"--glob=!dist/**",
|
|
52
|
+
"--glob=!build/**",
|
|
53
|
+
"--glob=!.next/**",
|
|
54
|
+
"--glob=!coverage/**",
|
|
55
|
+
"--glob=!.turbo/**",
|
|
56
|
+
"--glob=!.cache/**",
|
|
57
|
+
];
|
|
58
|
+
if (caseInsensitive) args.push("--ignore-case");
|
|
59
|
+
if (include) args.push("--glob", include);
|
|
60
|
+
args.push("-e", pattern, path);
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
execFile(
|
|
64
|
+
rgPath,
|
|
65
|
+
args,
|
|
66
|
+
{ maxBuffer: 10_000_000, timeout: 10_000 },
|
|
67
|
+
(error, stdout) => {
|
|
68
|
+
if (error && error.code !== 1) {
|
|
69
|
+
resolve([]);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const results: GrepMatch[] = [];
|
|
74
|
+
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
75
|
+
const trimmed = line.replace(/\r$/, "");
|
|
76
|
+
// Match file:linenum:content — greedily capture file path (handles Windows drive letters like E:\)
|
|
77
|
+
// then anchor on the numeric line number to avoid ambiguity with colons in file paths or content
|
|
78
|
+
const match = trimmed.match(/^(.+):(\d+):(.*)$/);
|
|
79
|
+
if (!match) continue;
|
|
80
|
+
const [, file, lineNum, content] = match;
|
|
81
|
+
if (!file || !lineNum) continue;
|
|
82
|
+
|
|
83
|
+
results.push({
|
|
84
|
+
file,
|
|
85
|
+
line: parseInt(lineNum),
|
|
86
|
+
match: content?.trim() ?? "",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (results.length >= 500) break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
resolve(results);
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function* walkFiles(
|
|
99
|
+
dir: string,
|
|
100
|
+
include?: string,
|
|
101
|
+
): AsyncGenerator<string> {
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
111
|
+
yield* walkFiles(join(dir, entry.name), include);
|
|
112
|
+
} else {
|
|
113
|
+
if (include) {
|
|
114
|
+
const ext = include.replace(/^\*/, "");
|
|
115
|
+
if (!entry.name.endsWith(ext)) continue;
|
|
116
|
+
}
|
|
117
|
+
yield join(dir, entry.name);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function grepFile(filePath: string, regex: RegExp): Promise<GrepMatch[]> {
|
|
123
|
+
try {
|
|
124
|
+
const content = await readFile(filePath, "utf-8");
|
|
125
|
+
const lines = content.split("\n");
|
|
126
|
+
const results: GrepMatch[] = [];
|
|
127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
if (regex.test(lines[i]!)) {
|
|
129
|
+
results.push({ file: filePath, line: i + 1, match: lines[i]!.trim() });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function grepFallback(
|
|
139
|
+
pattern: string,
|
|
140
|
+
path: string,
|
|
141
|
+
{
|
|
142
|
+
caseInsensitive = false,
|
|
143
|
+
include,
|
|
144
|
+
}: { caseInsensitive?: boolean; include?: string } = {},
|
|
145
|
+
): Promise<GrepMatch[]> {
|
|
146
|
+
const regex = new RegExp(pattern, caseInsensitive ? "i" : "");
|
|
147
|
+
const results: GrepMatch[] = [];
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const info = await stat(path);
|
|
151
|
+
if (info.isFile()) {
|
|
152
|
+
results.push(...(await grepFile(path, regex)));
|
|
153
|
+
} else {
|
|
154
|
+
for await (const file of walkFiles(path, include)) {
|
|
155
|
+
results.push(...(await grepFile(file, regex)));
|
|
156
|
+
if (results.length >= 500) break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return results.slice(0, 500);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function grep(
|
|
167
|
+
pattern: string,
|
|
168
|
+
path: string,
|
|
169
|
+
opts: { caseInsensitive?: boolean; include?: string } = {},
|
|
170
|
+
): Promise<GrepMatch[]> {
|
|
171
|
+
if (await isRgAvailable()) {
|
|
172
|
+
return grepWithRg(pattern, path, opts);
|
|
173
|
+
}
|
|
174
|
+
return grepFallback(pattern, path, opts);
|
|
175
|
+
}
|