@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,39 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { readFile, writeFile } from "fs/promises";
4
+ import { createPatch } from "diff";
5
+ import { DESCRIPTION, PROMPT } from "./prompt.js";
6
+ import { requestPermission } from "../../permissions";
7
+
8
+ export const FileEditTool = tool({
9
+ description: DESCRIPTION + "\n\n" + PROMPT,
10
+ inputSchema: z.object({
11
+ path: z.string().describe("The absolute file path to edit"),
12
+ old_string: z.string().describe("The exact string to replace"),
13
+ new_string: z.string().describe("The string to replace it with"),
14
+ }),
15
+ title: "EditFile",
16
+ execute: async ({ path, old_string, new_string }) => {
17
+ try {
18
+ const content = await readFile(path, "utf-8");
19
+
20
+ const matches = content.split(old_string).length - 1;
21
+ if (matches === 0)
22
+ return { success: false, error: "old_string not found in file" };
23
+ if (matches > 1)
24
+ return { success: false, error: `old_string matches ${matches} times` };
25
+
26
+ const newContent = content.replace(old_string, new_string);
27
+ const patch = createPatch(path, content, newContent);
28
+
29
+ const decision = await requestPermission("FileEditTool", { path, patch });
30
+ if (decision === "deny")
31
+ return { success: false, error: "User denied permission" };
32
+
33
+ await writeFile(path, newContent, "utf-8");
34
+ return { success: true, path, patch };
35
+ } catch (err) {
36
+ return { success: false, error: String(err) };
37
+ }
38
+ },
39
+ });
@@ -0,0 +1,5 @@
1
+ const MAX_LINES_TO_READ = 2000;
2
+ const MAX_LINE_LENGTH = 2000;
3
+
4
+ export const DESCRIPTION = "Read a file from the local filesystem.";
5
+ export const PROMPT = `Reads a file from the local filesystem. The file_path parameter must be an absolute path, not a relative path. By default, it reads up to ${MAX_LINES_TO_READ} lines starting from the beginning of the file. You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters. Any lines longer than ${MAX_LINE_LENGTH} characters will be truncated. For image files, the tool will display the image for you.`;
@@ -0,0 +1,34 @@
1
+ import { readFile } from "fs/promises";
2
+ import { tool } from "ai";
3
+ import { z } from "zod";
4
+ import { resolve } from "path";
5
+ import { PROMPT, DESCRIPTION } from "./prompt.js";
6
+ import { addLineNumbers, findSimilarFile } from "../../utils/file";
7
+
8
+ export const FileReadTool = tool({
9
+ description: DESCRIPTION + "\n\n" + PROMPT,
10
+ title: "ReadFile",
11
+ inputSchema: z.object({
12
+ path: z.string().describe("The file path to read"),
13
+ offset: z.number().optional().describe("Line offset to start reading from"),
14
+ limit: z.number().optional().describe("Max number of lines to read"),
15
+ }),
16
+ execute: async ({ path, offset, limit }) => {
17
+ try {
18
+ const absolutePath = resolve(path);
19
+ let lines = (await readFile(absolutePath, "utf-8")).split("\n");
20
+ const totalLines = lines.length;
21
+ if (offset) lines = lines.slice(offset);
22
+ if (limit) lines = lines.slice(0, limit);
23
+ const content = addLineNumbers(lines.join("\n"), offset ? offset + 1 : 1);
24
+ return { success: true, content, totalLines };
25
+ } catch (err) {
26
+ const similar = findSimilarFile(path);
27
+ return {
28
+ success: false,
29
+ error: String(err),
30
+ suggestion: similar ? `Did you mean: ${similar}?` : undefined,
31
+ };
32
+ }
33
+ },
34
+ });
@@ -0,0 +1,19 @@
1
+ import { cwd } from "process";
2
+
3
+ export const DESCRIPTION = "Write a file to the local filesystem.";
4
+
5
+ export const PROMPT = `Write a file to the local filesystem. Overwrites the existing file if there is one.
6
+
7
+ The current working directory is: ${cwd()}
8
+
9
+ Path resolution rules:
10
+ - If an absolute path is provided, use it as-is
11
+ - If a relative path is provided, resolve it against the current working directory above
12
+
13
+ Before using this tool:
14
+
15
+ 1. If overwriting an existing file, use ReadFile first to understand its contents.
16
+ If creating a new file, skip this step.
17
+
18
+ 2. Directory Verification (only applicable when creating new files):
19
+ - Use the LS tool to verify the parent directory exists and is the correct location`;
@@ -0,0 +1,34 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { writeFile, mkdir } from "fs/promises";
4
+ import { dirname } from "path";
5
+ import { DESCRIPTION, PROMPT } from "./prompt.js";
6
+ import {
7
+ requestPermission,
8
+ TOOLS_REQUIRING_PERMISSION,
9
+ } from "../../permissions";
10
+
11
+ export const FileWriteTool = tool({
12
+ description: DESCRIPTION + "\n\n" + PROMPT,
13
+ inputSchema: z.object({
14
+ path: z.string().describe("The absolute file path to write to"),
15
+ content: z.string().describe("The content to write to the file"),
16
+ }),
17
+ title: "WriteFile",
18
+ execute: async ({ path, content }) => {
19
+ try {
20
+ const decision = await requestPermission("FileWriteTool", {
21
+ path,
22
+ content,
23
+ });
24
+ if (decision === "deny")
25
+ return { success: false, error: "User denied permission" };
26
+
27
+ await mkdir(dirname(path), { recursive: true });
28
+ await writeFile(path, content, "utf-8");
29
+ return { success: true, path, content };
30
+ } catch (err) {
31
+ return { success: false, error: String(err) };
32
+ }
33
+ },
34
+ });
@@ -0,0 +1,11 @@
1
+ export const DESCRIPTION =
2
+ "Find files by name or path pattern using glob syntax.";
3
+ export const PROMPT = `Use this to find files by their name or path — not their contents.
4
+
5
+ Examples:
6
+ - "**/*.ts" — all TypeScript files
7
+ - "src/**/tool.ts" — all tool.ts files under src
8
+ - "**/*.test.js" — all test files
9
+
10
+ Use GrepTool instead if you need to search file contents.
11
+ Automatically excludes: node_modules, .git, dist, build.`;
@@ -0,0 +1,34 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { glob } from "glob";
4
+ import { DESCRIPTION, PROMPT } from "./prompt";
5
+ import { cwd } from "process";
6
+
7
+ export const GlobTool = tool({
8
+ title: "Glob",
9
+ description: DESCRIPTION + "\n\n" + PROMPT,
10
+ inputSchema: z.object({
11
+ pattern: z.string().describe("Glob pattern e.g. **/*.ts or src/**/tool.ts"),
12
+ path: z
13
+ .string()
14
+ .optional()
15
+ .describe("Directory to search in. Defaults to cwd."),
16
+ }),
17
+ execute: async ({ pattern, path }) => {
18
+ try {
19
+ const files = await glob(pattern, {
20
+ cwd: path ?? cwd(),
21
+ absolute: true,
22
+ ignore: [
23
+ "**/node_modules/**",
24
+ "**/.git/**",
25
+ "**/dist/**",
26
+ "**/build/**",
27
+ ],
28
+ });
29
+ return { success: true, files, numFiles: files.length };
30
+ } catch (err) {
31
+ return { success: false, error: String(err) };
32
+ }
33
+ },
34
+ });
@@ -0,0 +1,13 @@
1
+ export const DESCRIPTION = "Search for a pattern across files in a directory.";
2
+
3
+ export const PROMPT = `
4
+ - Fast content search tool that works with any codebase size
5
+ - Searches file contents using regular expressions
6
+ - Supports full regex syntax (e.g. "log.*Error", "function\\s+\\w+", etc.)
7
+ - Filter files by pattern with the include parameter (e.g. "*.js", "*.{ts,tsx}")
8
+ - Returns matching lines with file path and line number
9
+ - Results capped at 500 matches
10
+ - Automatically excludes: node_modules, .git, dist, build, .next, out, coverage
11
+ - Always use this instead of grep, findstr, or any bash search command
12
+ - Use this when you need to find files containing specific patterns or usages
13
+ `.trim();
@@ -0,0 +1,41 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { grep } from "../../utils/ripgrep";
4
+ import { DESCRIPTION, PROMPT } from "./prompt";
5
+ import { resolve } from "path";
6
+ import { cwd } from "process";
7
+
8
+ export const GrepTool = tool({
9
+ description: DESCRIPTION + "\n\n" + PROMPT,
10
+ title: "Grep",
11
+ inputSchema: z.object({
12
+ pattern: z
13
+ .string()
14
+ .describe("Regex or string pattern to search for in file contents"),
15
+ path: z
16
+ .string()
17
+ .optional()
18
+ .describe(
19
+ "Absolute path to file or directory to search. Defaults to current working directory if omitted.",
20
+ ),
21
+ caseInsensitive: z.boolean().default(false).describe("Ignore case"),
22
+ include: z
23
+ .string()
24
+ .optional()
25
+ .describe("File glob pattern e.g. *.ts or *.{ts,tsx}"),
26
+ }),
27
+ execute: async ({ pattern, path, caseInsensitive, include }) => {
28
+ try {
29
+ const resolvedPath = path ? resolve(path) : cwd();
30
+ const matches = await grep(pattern, resolvedPath, {
31
+ caseInsensitive,
32
+ include,
33
+ });
34
+ if (matches.length === 0)
35
+ return { success: true, matches: [], message: "No matches found" };
36
+ return { success: true, matches };
37
+ } catch (err) {
38
+ return { success: false, error: String(err) };
39
+ }
40
+ },
41
+ });
@@ -0,0 +1,10 @@
1
+ export const DESCRIPTION =
2
+ "Edit a specific section of a persistent memory file.";
3
+ export const PROMPT = `Updates a specific part of a memory file without rewriting the whole thing.
4
+
5
+ Use this to:
6
+ - Update a preference that changed
7
+ - Fix incorrect information
8
+ - Append or modify a specific section
9
+
10
+ Prefer this over MemoryWriteTool when only a small part needs to change.`;
@@ -0,0 +1,38 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { readFileSync, writeFileSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { MEMORY_DIR } from "../../utils/env";
6
+ import { DESCRIPTION, PROMPT } from "./prompt";
7
+
8
+ export const MemoryEditTool = tool({
9
+ title: "MemoryEdit",
10
+ description: DESCRIPTION + "\n\n" + PROMPT,
11
+ inputSchema: z.object({
12
+ file_path: z
13
+ .string()
14
+ .describe("Relative path inside memory dir e.g. MEMORY.md"),
15
+ old_string: z.string().describe("The string to replace"),
16
+ new_string: z.string().describe("The replacement string"),
17
+ }),
18
+ execute: async ({ file_path, old_string, new_string }) => {
19
+ try {
20
+ const fullPath = join(MEMORY_DIR, file_path);
21
+ if (!fullPath.startsWith(MEMORY_DIR)) {
22
+ return { success: false, error: "Invalid memory file path" };
23
+ }
24
+ if (!existsSync(fullPath)) {
25
+ return { success: false, error: "Memory file does not exist" };
26
+ }
27
+ const content = readFileSync(fullPath, "utf-8");
28
+ if (!content.includes(old_string)) {
29
+ return { success: false, error: "old_string not found in memory file" };
30
+ }
31
+ const updated = content.replace(old_string, new_string);
32
+ writeFileSync(fullPath, updated, "utf-8");
33
+ return { success: true, message: "Memory updated" };
34
+ } catch (err) {
35
+ return { success: false, error: String(err) };
36
+ }
37
+ },
38
+ });
@@ -0,0 +1,9 @@
1
+ export const DESCRIPTION = "Read a persistent memory file.";
2
+ export const PROMPT = `Reads from ~/.milo/memory/ to recall information from past sessions.
3
+
4
+ Use this at the start of a session to load context about:
5
+ - User preferences
6
+ - Project conventions
7
+ - Previously stored information
8
+
9
+ Always read MEMORY.md unless the user specifies otherwise.`;
@@ -0,0 +1,47 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { MEMORY_DIR } from "../../utils/env";
6
+ import { DESCRIPTION, PROMPT } from "./prompt";
7
+
8
+ const memoryCache = new Map<string, string>();
9
+
10
+ export const MemoryReadTool = tool({
11
+ title: "MemoryRead",
12
+ description: DESCRIPTION + "\n\n" + PROMPT,
13
+ inputSchema: z.object({
14
+ file_path: z
15
+ .string()
16
+ .describe("Relative path inside memory dir e.g. MEMORY.md"),
17
+ }),
18
+ execute: async ({ file_path }) => {
19
+ try {
20
+ if (memoryCache.has(file_path)) {
21
+ return {
22
+ success: true,
23
+ content: memoryCache.get(file_path)!,
24
+ cached: true,
25
+ };
26
+ }
27
+
28
+ const fullPath = join(MEMORY_DIR, file_path);
29
+ if (!fullPath.startsWith(MEMORY_DIR)) {
30
+ return { success: false, error: "Invalid memory file path" };
31
+ }
32
+ if (!existsSync(fullPath)) {
33
+ return {
34
+ success: true,
35
+ content: "",
36
+ message: "Memory file does not exist yet",
37
+ };
38
+ }
39
+ const content = readFileSync(fullPath, "utf-8");
40
+
41
+ memoryCache.set(file_path, content);
42
+ return { success: true, content };
43
+ } catch (err) {
44
+ return { success: false, error: String(err) };
45
+ }
46
+ },
47
+ });
@@ -0,0 +1,10 @@
1
+ export const DESCRIPTION = "Write content to a persistent memory file.";
2
+ export const PROMPT = `Saves information to ~/.milo/memory/ for use across sessions.
3
+
4
+ Use this to remember:
5
+ - User preferences and conventions
6
+ - Project-specific context (stack, patterns, rules)
7
+ - Anything the user explicitly asks you to remember
8
+
9
+ Always write to MEMORY.md unless the user specifies otherwise.
10
+ Content should be concise markdown.`;
@@ -0,0 +1,30 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { mkdirSync, writeFileSync } from "fs";
4
+ import { dirname, join } from "path";
5
+ import { MEMORY_DIR } from "../../utils/env";
6
+ import { DESCRIPTION, PROMPT } from "./prompt";
7
+
8
+ export const MemoryWriteTool = tool({
9
+ title: "MemoryWrite",
10
+ description: DESCRIPTION + "\n\n" + PROMPT,
11
+ inputSchema: z.object({
12
+ file_path: z
13
+ .string()
14
+ .describe("Relative path inside memory dir e.g. MEMORY.md"),
15
+ content: z.string().describe("Full content to write to the memory file"),
16
+ }),
17
+ execute: async ({ file_path, content }) => {
18
+ try {
19
+ const fullPath = join(MEMORY_DIR, file_path);
20
+ if (!fullPath.startsWith(MEMORY_DIR)) {
21
+ return { success: false, error: "Invalid memory file path" };
22
+ }
23
+ mkdirSync(dirname(fullPath), { recursive: true });
24
+ writeFileSync(fullPath, content, "utf-8");
25
+ return { success: true, message: "Memory saved" };
26
+ } catch (err) {
27
+ return { success: false, error: String(err) };
28
+ }
29
+ },
30
+ });
@@ -0,0 +1,26 @@
1
+ import { cwd } from "process";
2
+
3
+ export const DESCRIPTION =
4
+ "Orchestrate complex tasks by breaking them into parallel subtasks and delegating to specialized agents.";
5
+
6
+ export const PROMPT = `Use this tool when a task is too complex for a single agent — i.e. it requires creating multiple files, systems, or components that can be built independently.
7
+
8
+ Do not use this tool for simple tasks. Use it only when parallel execution provides a clear benefit.
9
+
10
+ Current working directory: ${cwd()}
11
+
12
+ How it works:
13
+ 1. A planner model breaks the task into subtasks
14
+ 2. Subtasks run in parallel where possible, sequentially where there are dependencies
15
+ 3. Each subtask returns a manifest of what it created
16
+ 4. A connector agent wires everything together
17
+
18
+ When to use:
19
+ - Building a full feature (API + types + tests)
20
+ - Scaffolding a project with multiple files
21
+ - Any task where subtasks are clearly independent
22
+
23
+ When NOT to use:
24
+ - Single file changes
25
+ - Questions or explanations
26
+ - Tasks that are inherently sequential`;
@@ -0,0 +1,20 @@
1
+ // OrchestratorTool.ts
2
+ import { z } from "zod";
3
+ import { DESCRIPTION, PROMPT } from "./prompt";
4
+ import { tool } from "ai";
5
+ import { Orchestrator } from "../../multi-agent/orchestrator/orchestrator";
6
+ import type { OnOrchestratorEvent } from "../../types";
7
+
8
+ export function createOrchestratorTool(onEvent?: OnOrchestratorEvent) {
9
+ return tool({
10
+ description: DESCRIPTION + "\n\n" + PROMPT,
11
+ inputSchema: z.object({
12
+ goal: z.string().describe("The complex task to orchestrate"),
13
+ }),
14
+ title: "Orchestrator",
15
+ execute: async ({ goal }) => {
16
+ const orchestrator = new Orchestrator(onEvent);
17
+ return await orchestrator.startTask(goal);
18
+ },
19
+ });
20
+ }
@@ -0,0 +1,13 @@
1
+ export const DESCRIPTION =
2
+ "Search through past Milo session history to recall previous conversations, code decisions, or context.";
3
+
4
+ export const PROMPT = `Use this tool when the user references something from a previous session, asks what was discussed before, or needs context from past conversations.
5
+
6
+ Triggers:
7
+ - "remember when we...", "last time we...", "what did we discuss about..."
8
+ - User references a past decision, file, or feature without current context
9
+ - You need historical context to answer accurately
10
+
11
+ Returns snippets from past session messages ranked by keyword relevance.
12
+
13
+ Do NOT use for current session context — that's already in your message history.`;
@@ -0,0 +1,47 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { DESCRIPTION, PROMPT } from "./prompt";
4
+ import { SESSIONS_DIR } from "../../utils/env";
5
+ import { grep } from "../../utils/ripgrep";
6
+
7
+ export const RecallTool = tool({
8
+ title: "Recall",
9
+ description: DESCRIPTION + "\n\n" + PROMPT,
10
+ inputSchema: z.object({
11
+ query: z
12
+ .string()
13
+ .describe("Keywords or phrase to search for in past sessions"),
14
+ max_results: z
15
+ .number()
16
+ .optional()
17
+ .default(5)
18
+ .describe("Max snippets to return"),
19
+ caseInsensitive: z
20
+ .boolean()
21
+ .optional()
22
+ .default(true)
23
+ .describe("Ignore case when searching"),
24
+ }),
25
+ execute: async ({ query, max_results, caseInsensitive }) => {
26
+ const matches = await grep(query, SESSIONS_DIR, {
27
+ caseInsensitive,
28
+ include: "*.json",
29
+ });
30
+
31
+ if (matches.length === 0) {
32
+ return { results: [], message: `No matches found for "${query}"` };
33
+ }
34
+
35
+ const top = matches.slice(0, max_results);
36
+
37
+ return {
38
+ query,
39
+ total_matches: matches.length,
40
+ results: top.map(({ file, line, match }) => ({
41
+ sessionFile: file.split(/[\\/]/).pop(),
42
+ line,
43
+ snippet: match,
44
+ })),
45
+ };
46
+ },
47
+ });
@@ -0,0 +1,16 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+
4
+ export const ThinkTool = tool({
5
+ title: "Think",
6
+ description:
7
+ "Use this tool to think through a problem before acting. No side effects — just a scratchpad for reasoning.",
8
+ inputSchema: z.object({
9
+ thought: z
10
+ .string()
11
+ .describe("Your reasoning, plan, or analysis before taking action"),
12
+ }),
13
+ execute: async ({ thought }) => {
14
+ return { success: true, message: "Your thought has been logged." };
15
+ },
16
+ });
@@ -0,0 +1,7 @@
1
+ export const DESCRIPTION = "Fetch the content of a URL and return its text.";
2
+ export const PROMPT = `Fetches a webpage and returns its text content with HTML stripped.
3
+
4
+ Guidelines:
5
+ - Use this to read documentation, articles, or any webpage
6
+ - Content is truncated to 8000 characters
7
+ - Always provide a full URL including https://`;
@@ -0,0 +1,33 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { DESCRIPTION, PROMPT } from "./prompt.js";
4
+
5
+ export const WebFetchTool = tool({
6
+ description: DESCRIPTION + "\n\n" + PROMPT,
7
+ inputSchema: z.object({
8
+ url: z.string().describe("The URL to fetch"),
9
+ }),
10
+ title: "WebFetch",
11
+ execute: async ({ url }) => {
12
+ try {
13
+ const res = await fetch(url, {
14
+ headers: {
15
+ "User-Agent": "Mozilla/5.0 (compatible; Milo/1.0)",
16
+ },
17
+ signal: AbortSignal.timeout(10000),
18
+ });
19
+ const html = await res.text();
20
+ // strip tags, keep text
21
+ const text = html
22
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
23
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
24
+ .replace(/<[^>]+>/g, " ")
25
+ .replace(/\s+/g, " ")
26
+ .trim()
27
+ .slice(0, 8000);
28
+ return { success: true, url, content: text };
29
+ } catch (err) {
30
+ return { success: false, error: String(err) };
31
+ }
32
+ },
33
+ });
@@ -0,0 +1,8 @@
1
+ export const DESCRIPTION =
2
+ "Search the web using DuckDuckGo. No API key required.";
3
+ export const PROMPT = `Searches the web and returns titles, URLs, and snippets.
4
+
5
+ Guidelines:
6
+ - Use this when the user asks about current events, documentation, or anything that needs live web data
7
+ - Use WebFetchTool to read the full content of a result URL
8
+ - Keep queries short and specific for best results`;
@@ -0,0 +1,49 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { DESCRIPTION, PROMPT } from "./prompt.js";
4
+
5
+ export const WebSearchTool = tool({
6
+ description: DESCRIPTION + "\n\n" + PROMPT,
7
+ inputSchema: z.object({
8
+ query: z.string().describe("The search query"),
9
+ }),
10
+ title: "WebSearch",
11
+ execute: async ({ query }) => {
12
+ try {
13
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
14
+ const res = await fetch(url, {
15
+ headers: {
16
+ "User-Agent": "Mozilla/5.0 (compatible; Milo/1.0)",
17
+ },
18
+ signal: AbortSignal.timeout(10000),
19
+ });
20
+ const html = await res.text();
21
+
22
+ const results: { title: string; url: string; snippet: string }[] = [];
23
+ const resultRegex =
24
+ /<a class="result__a" href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
25
+ let match;
26
+ while ((match = resultRegex.exec(html)) !== null && results.length < 8) {
27
+ results.push({
28
+ url: match[1] ?? "",
29
+ title: match[2]?.trim() ?? "",
30
+ snippet: match[3]?.replace(/<[^>]+>/g, "").trim() ?? "",
31
+ });
32
+ }
33
+
34
+ if (results.length === 0) {
35
+ // fallback — just strip html
36
+ const text = html
37
+ .replace(/<[^>]+>/g, " ")
38
+ .replace(/\s+/g, " ")
39
+ .trim()
40
+ .slice(0, 4000);
41
+ return { success: true, query, results: [], raw: text };
42
+ }
43
+
44
+ return { success: true, query, results };
45
+ } catch (err) {
46
+ return { success: false, error: String(err) };
47
+ }
48
+ },
49
+ });