@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,263 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { type Key } from "ink";
|
|
3
|
+
import { Cursor } from "../utils/Cursor";
|
|
4
|
+
import {
|
|
5
|
+
getImageFromClipboard,
|
|
6
|
+
CLIPBOARD_ERROR_MESSAGE,
|
|
7
|
+
} from "../utils/imagePaste";
|
|
8
|
+
|
|
9
|
+
const IMAGE_PLACEHOLDER = "[Image pasted]";
|
|
10
|
+
|
|
11
|
+
type MaybeCursor = void | Cursor;
|
|
12
|
+
type InputHandler = (input: string) => MaybeCursor;
|
|
13
|
+
type InputMapper = (input: string) => MaybeCursor;
|
|
14
|
+
|
|
15
|
+
function mapInput(input_map: Array<[string, InputHandler]>): InputMapper {
|
|
16
|
+
return function (input: string): MaybeCursor {
|
|
17
|
+
const handler = new Map(input_map).get(input) ?? (() => {});
|
|
18
|
+
return handler(input);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type UseTextInputProps = {
|
|
23
|
+
value: string;
|
|
24
|
+
onChange: (value: string) => void;
|
|
25
|
+
onSubmit?: (value: string) => void;
|
|
26
|
+
onExit?: () => void;
|
|
27
|
+
onExitMessage?: (show: boolean, key?: string) => void;
|
|
28
|
+
onMessage?: (show: boolean, message?: string) => void;
|
|
29
|
+
onHistoryUp?: () => void;
|
|
30
|
+
onHistoryDown?: () => void;
|
|
31
|
+
onHistoryReset?: () => void;
|
|
32
|
+
mask?: string;
|
|
33
|
+
multiline?: boolean;
|
|
34
|
+
cursorChar: string;
|
|
35
|
+
invert: (text: string) => string;
|
|
36
|
+
columns: number;
|
|
37
|
+
onImagePaste?: (base64Image: string) => void;
|
|
38
|
+
disableCursorMovementForUpDownKeys?: boolean;
|
|
39
|
+
externalOffset: number;
|
|
40
|
+
onOffsetChange: (offset: number) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type UseTextInputResult = {
|
|
44
|
+
renderedValue: string;
|
|
45
|
+
onInput: (input: string, key: Key) => void;
|
|
46
|
+
offset: number;
|
|
47
|
+
setOffset: (offset: number) => void;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function useTextInput({
|
|
51
|
+
value: originalValue,
|
|
52
|
+
onChange,
|
|
53
|
+
onSubmit,
|
|
54
|
+
onExit,
|
|
55
|
+
onExitMessage,
|
|
56
|
+
onMessage,
|
|
57
|
+
onHistoryUp,
|
|
58
|
+
onHistoryDown,
|
|
59
|
+
onHistoryReset,
|
|
60
|
+
mask = "",
|
|
61
|
+
multiline = false,
|
|
62
|
+
cursorChar,
|
|
63
|
+
invert,
|
|
64
|
+
columns,
|
|
65
|
+
onImagePaste,
|
|
66
|
+
disableCursorMovementForUpDownKeys = false,
|
|
67
|
+
externalOffset,
|
|
68
|
+
onOffsetChange,
|
|
69
|
+
}: UseTextInputProps): UseTextInputResult {
|
|
70
|
+
const offset = externalOffset;
|
|
71
|
+
const setOffset = onOffsetChange;
|
|
72
|
+
const cursor = Cursor.fromText(originalValue, columns, offset);
|
|
73
|
+
const [imagePasteErrorTimeout, setImagePasteErrorTimeout] =
|
|
74
|
+
useState<NodeJS.Timeout | null>(null);
|
|
75
|
+
|
|
76
|
+
function maybeClearImagePasteErrorTimeout() {
|
|
77
|
+
if (!imagePasteErrorTimeout) return;
|
|
78
|
+
clearTimeout(imagePasteErrorTimeout);
|
|
79
|
+
setImagePasteErrorTimeout(null);
|
|
80
|
+
onMessage?.(false);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handleCtrlC() {
|
|
84
|
+
maybeClearImagePasteErrorTimeout();
|
|
85
|
+
if (originalValue) {
|
|
86
|
+
onChange("");
|
|
87
|
+
onHistoryReset?.();
|
|
88
|
+
} else {
|
|
89
|
+
onExit?.();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleEscape() {
|
|
94
|
+
maybeClearImagePasteErrorTimeout();
|
|
95
|
+
if (originalValue) {
|
|
96
|
+
onChange("");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function clear() {
|
|
101
|
+
return Cursor.fromText("", columns, 0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleCtrlD(): MaybeCursor {
|
|
105
|
+
maybeClearImagePasteErrorTimeout();
|
|
106
|
+
if (cursor.text === "") {
|
|
107
|
+
onExit?.();
|
|
108
|
+
return cursor;
|
|
109
|
+
}
|
|
110
|
+
return cursor.del();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function tryImagePaste(): MaybeCursor {
|
|
114
|
+
const base64Image = getImageFromClipboard();
|
|
115
|
+
if (base64Image === null) {
|
|
116
|
+
if (process.platform !== "darwin") return cursor;
|
|
117
|
+
onMessage?.(true, CLIPBOARD_ERROR_MESSAGE);
|
|
118
|
+
maybeClearImagePasteErrorTimeout();
|
|
119
|
+
setImagePasteErrorTimeout(
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
onMessage?.(false);
|
|
122
|
+
}, 4000),
|
|
123
|
+
);
|
|
124
|
+
return cursor;
|
|
125
|
+
}
|
|
126
|
+
onImagePaste?.(base64Image);
|
|
127
|
+
return cursor.insert(IMAGE_PLACEHOLDER);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const handleCtrl = mapInput([
|
|
131
|
+
["a", () => cursor.startOfLine()],
|
|
132
|
+
["b", () => cursor.left()],
|
|
133
|
+
[
|
|
134
|
+
"c",
|
|
135
|
+
() => {
|
|
136
|
+
handleCtrlC();
|
|
137
|
+
return cursor;
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
["d", handleCtrlD],
|
|
141
|
+
["e", () => cursor.endOfLine()],
|
|
142
|
+
["f", () => cursor.right()],
|
|
143
|
+
["h", () => cursor.backspace()],
|
|
144
|
+
["k", () => cursor.deleteToLineEnd()],
|
|
145
|
+
["l", () => clear()],
|
|
146
|
+
["n", () => downOrHistoryDown()],
|
|
147
|
+
["p", () => upOrHistoryUp()],
|
|
148
|
+
["u", () => cursor.deleteToLineStart()],
|
|
149
|
+
["v", tryImagePaste],
|
|
150
|
+
["w", () => cursor.deleteWordBefore()],
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const handleMeta = mapInput([
|
|
154
|
+
["b", () => cursor.prevWord()],
|
|
155
|
+
["f", () => cursor.nextWord()],
|
|
156
|
+
["d", () => cursor.deleteWordAfter()],
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
function handleEnter(key: Key): MaybeCursor {
|
|
160
|
+
if (
|
|
161
|
+
multiline &&
|
|
162
|
+
cursor.offset > 0 &&
|
|
163
|
+
cursor.text[cursor.offset - 1] === "\\"
|
|
164
|
+
) {
|
|
165
|
+
return cursor.backspace().insert("\n");
|
|
166
|
+
}
|
|
167
|
+
if (key.meta) return cursor.insert("\n");
|
|
168
|
+
onSubmit?.(originalValue);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function upOrHistoryUp(): MaybeCursor {
|
|
172
|
+
if (disableCursorMovementForUpDownKeys) {
|
|
173
|
+
onHistoryUp?.();
|
|
174
|
+
return cursor;
|
|
175
|
+
}
|
|
176
|
+
const cursorUp = cursor.up();
|
|
177
|
+
if (cursorUp.equals(cursor)) onHistoryUp?.();
|
|
178
|
+
return cursorUp;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function downOrHistoryDown(): MaybeCursor {
|
|
182
|
+
if (disableCursorMovementForUpDownKeys) {
|
|
183
|
+
onHistoryDown?.();
|
|
184
|
+
return cursor;
|
|
185
|
+
}
|
|
186
|
+
const cursorDown = cursor.down();
|
|
187
|
+
if (cursorDown.equals(cursor)) onHistoryDown?.();
|
|
188
|
+
return cursorDown;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function mapKey(key: Key): InputMapper {
|
|
192
|
+
switch (true) {
|
|
193
|
+
case key.escape:
|
|
194
|
+
return () => {
|
|
195
|
+
handleEscape();
|
|
196
|
+
return cursor;
|
|
197
|
+
};
|
|
198
|
+
case key.leftArrow && (key.ctrl || key.meta):
|
|
199
|
+
return () => cursor.prevWord();
|
|
200
|
+
case key.rightArrow && (key.ctrl || key.meta):
|
|
201
|
+
return () => cursor.nextWord();
|
|
202
|
+
case key.backspace || key.delete:
|
|
203
|
+
return key.meta
|
|
204
|
+
? () => cursor.deleteWordBefore()
|
|
205
|
+
: () => cursor.backspace();
|
|
206
|
+
case key.delete:
|
|
207
|
+
return key.meta ? () => cursor.deleteToLineEnd() : () => cursor.del();
|
|
208
|
+
case key.ctrl:
|
|
209
|
+
return handleCtrl;
|
|
210
|
+
case key.home:
|
|
211
|
+
return () => cursor.startOfLine();
|
|
212
|
+
case key.end:
|
|
213
|
+
return () => cursor.endOfLine();
|
|
214
|
+
case key.pageDown:
|
|
215
|
+
return () => cursor.endOfLine();
|
|
216
|
+
case key.pageUp:
|
|
217
|
+
return () => cursor.startOfLine();
|
|
218
|
+
case key.meta:
|
|
219
|
+
return handleMeta;
|
|
220
|
+
case key.return:
|
|
221
|
+
return () => handleEnter(key);
|
|
222
|
+
case key.tab:
|
|
223
|
+
return () => {};
|
|
224
|
+
case key.upArrow:
|
|
225
|
+
return upOrHistoryUp;
|
|
226
|
+
case key.downArrow:
|
|
227
|
+
return downOrHistoryDown;
|
|
228
|
+
case key.leftArrow:
|
|
229
|
+
return () => cursor.left();
|
|
230
|
+
case key.rightArrow:
|
|
231
|
+
return () => cursor.right();
|
|
232
|
+
}
|
|
233
|
+
return function (input: string) {
|
|
234
|
+
switch (true) {
|
|
235
|
+
case input === "\x1b[H" || input === "\x1b[1~":
|
|
236
|
+
return cursor.startOfLine();
|
|
237
|
+
case input === "\x1b[F" || input === "\x1b[4~":
|
|
238
|
+
return cursor.endOfLine();
|
|
239
|
+
default:
|
|
240
|
+
return cursor.insert(input.replace(/\r/g, "\n"));
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function onInput(input: string, key: Key): void {
|
|
246
|
+
const nextCursor = mapKey(key)(input);
|
|
247
|
+
if (nextCursor) {
|
|
248
|
+
if (!cursor.equals(nextCursor)) {
|
|
249
|
+
setOffset(nextCursor.offset);
|
|
250
|
+
if (cursor.text !== nextCursor.text) {
|
|
251
|
+
onChange(nextCursor.text);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
onInput,
|
|
259
|
+
renderedValue: cursor.render(cursorChar, mask, invert),
|
|
260
|
+
offset,
|
|
261
|
+
setOffset,
|
|
262
|
+
};
|
|
263
|
+
}
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const tick = "✔";
|
|
2
|
+
export const cross = "✘";
|
|
3
|
+
export const star = "★";
|
|
4
|
+
export const pointer = "❯";
|
|
5
|
+
export const pointerSmall = "›";
|
|
6
|
+
export const info = "ℹ";
|
|
7
|
+
export const warning = "⚠";
|
|
8
|
+
export const bullet = "●";
|
|
9
|
+
export const dot = "․";
|
|
10
|
+
export const ellipsis = "…";
|
|
11
|
+
export const line = "─";
|
|
12
|
+
export const lineVertical = "│";
|
|
13
|
+
export const triangleRight = "▶";
|
|
14
|
+
export const lozenge = "◆";
|
|
15
|
+
export const lozengeOutline = "◇";
|
|
16
|
+
export const circle = "◯";
|
|
17
|
+
export const circleFilled = "◉";
|
|
18
|
+
export const radioOn = "◉";
|
|
19
|
+
export const radioOff = "◯";
|
|
20
|
+
export const checkboxOn = "☒";
|
|
21
|
+
export const checkboxOff = "☐";
|
|
22
|
+
export const arrowUp = "↑";
|
|
23
|
+
export const arrowDown = "↓";
|
|
24
|
+
export const arrowLeft = "←";
|
|
25
|
+
export const arrowRight = "→";
|
|
26
|
+
export const cornerTopLeft = "╭";
|
|
27
|
+
export const cornerBottomLeft = "╰";
|
|
28
|
+
export const cornerTopRight = "╮";
|
|
29
|
+
export const cornerBottomRight = "╯";
|
|
30
|
+
export const diamond = "◆";
|
|
31
|
+
export const upDownArrow = "↑↓";
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConnectorSystemPrompt,
|
|
3
|
+
getOrchestratorAgentSystemPrompt,
|
|
4
|
+
} from "../../utils/systemPrompt";
|
|
5
|
+
import { agentTools, orchestratorAgentTools } from "../../utils/tools";
|
|
6
|
+
import { runLLM } from "../../utils/llm";
|
|
7
|
+
import type { OnOrchestratorEvent } from "../../types";
|
|
8
|
+
|
|
9
|
+
export async function spawnAgent(
|
|
10
|
+
subtask: string,
|
|
11
|
+
mode = "agent",
|
|
12
|
+
onEvent?: OnOrchestratorEvent,
|
|
13
|
+
taskId?: string,
|
|
14
|
+
) {
|
|
15
|
+
onEvent?.({ type: "agent_start", taskId: taskId ?? "?", subtask });
|
|
16
|
+
|
|
17
|
+
const system =
|
|
18
|
+
mode === "connector"
|
|
19
|
+
? await getConnectorSystemPrompt()
|
|
20
|
+
: await getOrchestratorAgentSystemPrompt();
|
|
21
|
+
|
|
22
|
+
const tools =
|
|
23
|
+
mode === "connector" ? orchestratorAgentTools : orchestratorAgentTools;
|
|
24
|
+
|
|
25
|
+
const { text } = await runLLM({
|
|
26
|
+
system,
|
|
27
|
+
prompt: subtask,
|
|
28
|
+
tools,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
onEvent?.({ type: "agent_done", taskId: taskId ?? "?", result: text });
|
|
32
|
+
return { subtask, result: text };
|
|
33
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { chatWithModel } from "../../utils/chat";
|
|
2
|
+
import { taskSchema } from "../schemas";
|
|
3
|
+
import { spawnAgent } from "../agent/agent";
|
|
4
|
+
import type { Plan } from "../types";
|
|
5
|
+
import { safeParseJSON } from "../../utils/json";
|
|
6
|
+
import type { OnOrchestratorEvent } from "../../types";
|
|
7
|
+
|
|
8
|
+
export class Orchestrator {
|
|
9
|
+
constructor(private onEvent?: OnOrchestratorEvent) {}
|
|
10
|
+
|
|
11
|
+
private async create_plan(prompt: string) {
|
|
12
|
+
const { text } =
|
|
13
|
+
await chatWithModel(`Break this task into subtasks. Respond with ONLY valid JSON, no markdown, no explanation:
|
|
14
|
+
{
|
|
15
|
+
"tasks": [
|
|
16
|
+
{ "id": "1", "subtask": "...", "tools": [], "dependsOn": [] }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
Rules:
|
|
20
|
+
- dependsOn must be an array (empty if no dependencies)
|
|
21
|
+
- Each subtask must include the full absolute path of the file it creates
|
|
22
|
+
- Keep subtasks small and self-contained
|
|
23
|
+
|
|
24
|
+
Task: ${prompt}`);
|
|
25
|
+
|
|
26
|
+
const clean = text.replace(/```json|```/g, "").trim();
|
|
27
|
+
const plan = taskSchema.parse(safeParseJSON(clean));
|
|
28
|
+
this.onEvent?.({ type: "plan_created", tasks: plan.tasks });
|
|
29
|
+
return plan;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async spawnAgents(
|
|
33
|
+
plan: Plan,
|
|
34
|
+
results: Record<string, string>,
|
|
35
|
+
completed: Set<string>,
|
|
36
|
+
) {
|
|
37
|
+
const runTask = async (taskId: string) => {
|
|
38
|
+
const t = plan.tasks.find((t) => t.id === taskId)!;
|
|
39
|
+
|
|
40
|
+
if (t.dependsOn?.length) {
|
|
41
|
+
await Promise.all(
|
|
42
|
+
t.dependsOn.map(
|
|
43
|
+
(dep) =>
|
|
44
|
+
new Promise<void>((resolve) => {
|
|
45
|
+
const interval = setInterval(() => {
|
|
46
|
+
if (completed.has(dep)) {
|
|
47
|
+
clearInterval(interval);
|
|
48
|
+
resolve();
|
|
49
|
+
}
|
|
50
|
+
}, 100);
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { result } = await spawnAgent(
|
|
57
|
+
t.subtask,
|
|
58
|
+
"agent",
|
|
59
|
+
this.onEvent,
|
|
60
|
+
taskId,
|
|
61
|
+
);
|
|
62
|
+
results[taskId] = result;
|
|
63
|
+
completed.add(taskId);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await Promise.all(plan.tasks.map((t) => runTask(t.id)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async complete(plan: Plan, results: Record<string, string>) {
|
|
70
|
+
this.onEvent?.({ type: "connecting" });
|
|
71
|
+
const manifestSummary = Object.entries(results)
|
|
72
|
+
.map(([id, result]) => `[${id}]: ${result}`)
|
|
73
|
+
.join("\n");
|
|
74
|
+
|
|
75
|
+
const { result } = await spawnAgent(
|
|
76
|
+
`Manifest:\n${manifestSummary}\n\nWire everything together — fix imports, ensure consistency, resolve integration issues.`,
|
|
77
|
+
"connector",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
this.onEvent?.({ type: "done" });
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
plan: plan.tasks.map((t) => t.id),
|
|
84
|
+
result: results,
|
|
85
|
+
connection: result,
|
|
86
|
+
summary: `Completed ${plan.tasks.length} subtasks and connected all files.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public async startTask(task: string) {
|
|
91
|
+
try {
|
|
92
|
+
const results: Record<string, string> = {};
|
|
93
|
+
const completed = new Set<string>();
|
|
94
|
+
|
|
95
|
+
const plan = await this.create_plan(task);
|
|
96
|
+
await this.spawnAgents(plan, results, completed);
|
|
97
|
+
const output = await this.complete(plan, results);
|
|
98
|
+
return output;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type PermissionDecision = "allow" | "allow_session" | "deny";
|
|
2
|
+
|
|
3
|
+
const sessionAllowed = new Set<string>();
|
|
4
|
+
|
|
5
|
+
export function isSessionAllowed(toolName: string): boolean {
|
|
6
|
+
return sessionAllowed.has(toolName);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function allowInSession(toolName: string): void {
|
|
10
|
+
sessionAllowed.add(toolName);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const TOOLS_REQUIRING_PERMISSION = new Set([
|
|
14
|
+
"FileWriteTool",
|
|
15
|
+
"FileEditTool",
|
|
16
|
+
"BashTool",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
type PendingPermission = {
|
|
20
|
+
resolve: (decision: PermissionDecision) => void;
|
|
21
|
+
toolName: string;
|
|
22
|
+
input: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let pending: PendingPermission | null = null;
|
|
26
|
+
|
|
27
|
+
export function requestPermission(
|
|
28
|
+
toolName: string,
|
|
29
|
+
input: unknown,
|
|
30
|
+
): Promise<PermissionDecision> {
|
|
31
|
+
if (isSessionAllowed(toolName)) return Promise.resolve("allow");
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
pending = { resolve, toolName, input };
|
|
34
|
+
permissionListeners.forEach((l) => l(pending!));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type PermissionListener = (p: PendingPermission) => void;
|
|
39
|
+
const permissionListeners: PermissionListener[] = [];
|
|
40
|
+
|
|
41
|
+
export function onPermissionRequest(listener: PermissionListener) {
|
|
42
|
+
permissionListeners.push(listener);
|
|
43
|
+
return () => {
|
|
44
|
+
const i = permissionListeners.indexOf(listener);
|
|
45
|
+
if (i >= 0) permissionListeners.splice(i, 1);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolvePermission(decision: PermissionDecision): void {
|
|
50
|
+
if (!pending) return;
|
|
51
|
+
if (decision === "allow_session") allowInSession(pending.toolName);
|
|
52
|
+
pending.resolve(decision);
|
|
53
|
+
pending = null;
|
|
54
|
+
}
|