fraude-code 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/README.md +68 -0
- package/dist/index.js +179297 -0
- package/package.json +88 -0
- package/src/agent/agent.ts +475 -0
- package/src/agent/contextManager.ts +141 -0
- package/src/agent/index.ts +14 -0
- package/src/agent/pendingChanges.ts +270 -0
- package/src/agent/prompts/AskPrompt.txt +10 -0
- package/src/agent/prompts/FastPrompt.txt +40 -0
- package/src/agent/prompts/PlannerPrompt.txt +51 -0
- package/src/agent/prompts/ReviewerPrompt.txt +57 -0
- package/src/agent/prompts/WorkerPrompt.txt +33 -0
- package/src/agent/subagents/askAgent.ts +37 -0
- package/src/agent/subagents/extractionAgent.ts +123 -0
- package/src/agent/subagents/fastAgent.ts +45 -0
- package/src/agent/subagents/managerAgent.ts +36 -0
- package/src/agent/subagents/relationAgent.ts +76 -0
- package/src/agent/subagents/researchSubAgent.ts +79 -0
- package/src/agent/subagents/reviewerSubAgent.ts +42 -0
- package/src/agent/subagents/workerSubAgent.ts +42 -0
- package/src/agent/tools/bashTool.ts +94 -0
- package/src/agent/tools/descriptions/bash.txt +47 -0
- package/src/agent/tools/descriptions/edit.txt +7 -0
- package/src/agent/tools/descriptions/glob.txt +4 -0
- package/src/agent/tools/descriptions/grep.txt +8 -0
- package/src/agent/tools/descriptions/lsp.txt +20 -0
- package/src/agent/tools/descriptions/plan.txt +3 -0
- package/src/agent/tools/descriptions/read.txt +9 -0
- package/src/agent/tools/descriptions/todo.txt +12 -0
- package/src/agent/tools/descriptions/write.txt +8 -0
- package/src/agent/tools/editTool.ts +44 -0
- package/src/agent/tools/globTool.ts +59 -0
- package/src/agent/tools/grepTool.ts +343 -0
- package/src/agent/tools/lspTool.ts +429 -0
- package/src/agent/tools/planTool.ts +118 -0
- package/src/agent/tools/readTool.ts +78 -0
- package/src/agent/tools/rememberTool.ts +91 -0
- package/src/agent/tools/testRunnerTool.ts +77 -0
- package/src/agent/tools/testTool.ts +44 -0
- package/src/agent/tools/todoTool.ts +224 -0
- package/src/agent/tools/writeTool.ts +33 -0
- package/src/commands/COMMANDS.ts +38 -0
- package/src/commands/cerebras/auth.ts +27 -0
- package/src/commands/cerebras/index.ts +31 -0
- package/src/commands/forget.ts +29 -0
- package/src/commands/google/auth.ts +24 -0
- package/src/commands/google/index.ts +31 -0
- package/src/commands/groq/add_model.ts +60 -0
- package/src/commands/groq/auth.ts +24 -0
- package/src/commands/groq/index.ts +33 -0
- package/src/commands/index.ts +65 -0
- package/src/commands/knowledge.ts +92 -0
- package/src/commands/log.ts +32 -0
- package/src/commands/mistral/auth.ts +27 -0
- package/src/commands/mistral/index.ts +31 -0
- package/src/commands/model/index.ts +145 -0
- package/src/commands/models/index.ts +16 -0
- package/src/commands/ollama/index.ts +29 -0
- package/src/commands/openrouter/add_model.ts +64 -0
- package/src/commands/openrouter/auth.ts +24 -0
- package/src/commands/openrouter/index.ts +33 -0
- package/src/commands/remember.ts +48 -0
- package/src/commands/serve.ts +31 -0
- package/src/commands/session/index.ts +21 -0
- package/src/commands/usage.ts +15 -0
- package/src/commands/visualize.ts +773 -0
- package/src/components/App.tsx +55 -0
- package/src/components/IntroComponent.tsx +70 -0
- package/src/components/LoaderComponent.tsx +68 -0
- package/src/components/OutputRenderer.tsx +88 -0
- package/src/components/SettingsRenderer.tsx +23 -0
- package/src/components/input/CommandSuggestions.tsx +41 -0
- package/src/components/input/FileSuggestions.tsx +61 -0
- package/src/components/input/InputBox.tsx +371 -0
- package/src/components/output/CheckpointView.tsx +13 -0
- package/src/components/output/CommandView.tsx +13 -0
- package/src/components/output/CommentView.tsx +12 -0
- package/src/components/output/ConfirmationView.tsx +179 -0
- package/src/components/output/ContextUsage.tsx +62 -0
- package/src/components/output/DiffView.tsx +202 -0
- package/src/components/output/ErrorView.tsx +14 -0
- package/src/components/output/InteractiveServerView.tsx +69 -0
- package/src/components/output/KnowledgeView.tsx +220 -0
- package/src/components/output/MarkdownView.tsx +15 -0
- package/src/components/output/ModelSelectView.tsx +71 -0
- package/src/components/output/ReasoningView.tsx +21 -0
- package/src/components/output/ToolCallView.tsx +45 -0
- package/src/components/settings/ModelList.tsx +250 -0
- package/src/components/settings/TokenUsage.tsx +274 -0
- package/src/config/schema.ts +19 -0
- package/src/config/settings.ts +229 -0
- package/src/index.tsx +100 -0
- package/src/parsers/tree-sitter-python.wasm +0 -0
- package/src/providers/providers.ts +71 -0
- package/src/services/PluginLoader.ts +123 -0
- package/src/services/cerebras.ts +69 -0
- package/src/services/embeddingService.ts +229 -0
- package/src/services/google.ts +65 -0
- package/src/services/graphSerializer.ts +248 -0
- package/src/services/groq.ts +23 -0
- package/src/services/knowledgeOrchestrator.ts +286 -0
- package/src/services/mistral.ts +79 -0
- package/src/services/ollama.ts +109 -0
- package/src/services/openrouter.ts +23 -0
- package/src/services/symbolExtractor.ts +277 -0
- package/src/store/useFraudeStore.ts +123 -0
- package/src/store/useSettingsStore.ts +38 -0
- package/src/theme.ts +26 -0
- package/src/types/Agent.ts +147 -0
- package/src/types/CommandDefinition.ts +8 -0
- package/src/types/Model.ts +94 -0
- package/src/types/OutputItem.ts +24 -0
- package/src/types/PluginContext.ts +55 -0
- package/src/types/TokenUsage.ts +5 -0
- package/src/types/assets.d.ts +4 -0
- package/src/utils/agentCognition.ts +1152 -0
- package/src/utils/fileSuggestions.ts +111 -0
- package/src/utils/index.ts +17 -0
- package/src/utils/initFraude.ts +8 -0
- package/src/utils/logger.ts +24 -0
- package/src/utils/lspClient.ts +1415 -0
- package/src/utils/paths.ts +24 -0
- package/src/utils/queryHandler.ts +227 -0
- package/src/utils/router.ts +278 -0
- package/src/utils/streamHandler.ts +132 -0
- package/src/utils/treeSitterQueries.ts +125 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { platform, homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
export function getConfigDir(appName: string): string {
|
|
5
|
+
const osPlatform = platform();
|
|
6
|
+
const home = homedir();
|
|
7
|
+
|
|
8
|
+
switch (osPlatform) {
|
|
9
|
+
case "win32":
|
|
10
|
+
return join(
|
|
11
|
+
process.env.APPDATA || join(home, "AppData", "Roaming"),
|
|
12
|
+
appName,
|
|
13
|
+
);
|
|
14
|
+
case "darwin":
|
|
15
|
+
return join(home, "Library", "Application Support", appName);
|
|
16
|
+
case "linux":
|
|
17
|
+
return join(
|
|
18
|
+
process.env.XDG_CONFIG_HOME || join(home, ".config"),
|
|
19
|
+
appName,
|
|
20
|
+
);
|
|
21
|
+
default:
|
|
22
|
+
return join(home, `.${appName}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import useFraudeStore from "@/store/useFraudeStore";
|
|
2
|
+
import CommandCenter from "@/commands";
|
|
3
|
+
import log from "./logger";
|
|
4
|
+
import { resetStreamState } from "./streamHandler";
|
|
5
|
+
import pendingChanges from "@/agent/pendingChanges";
|
|
6
|
+
import { getManagerAgent } from "@/agent/subagents/managerAgent";
|
|
7
|
+
import {
|
|
8
|
+
getNextTodo,
|
|
9
|
+
getTodoById,
|
|
10
|
+
hasPendingTodos,
|
|
11
|
+
} from "@/agent/tools/todoTool";
|
|
12
|
+
import { getWorkerSubAgent } from "@/agent/subagents/workerSubAgent";
|
|
13
|
+
import { getReviewerSubAgent } from "@/agent/subagents/reviewerSubAgent";
|
|
14
|
+
import type { TodoItem } from "@/agent/tools/todoTool";
|
|
15
|
+
import getFastAgent from "@/agent/subagents/fastAgent";
|
|
16
|
+
import getAskAgent from "@/agent/subagents/askAgent";
|
|
17
|
+
|
|
18
|
+
const { updateOutput } = useFraudeStore.getState();
|
|
19
|
+
|
|
20
|
+
const checkAbort = () => {
|
|
21
|
+
const abortController = useFraudeStore.getState().abortController;
|
|
22
|
+
if (abortController?.signal.aborted) {
|
|
23
|
+
const error = new Error("Aborted");
|
|
24
|
+
error.name = "AbortError";
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getTaskContext = (task: TodoItem) => {
|
|
30
|
+
const context = task.context;
|
|
31
|
+
const notes = task.notes;
|
|
32
|
+
|
|
33
|
+
return `Task: ${task.description}
|
|
34
|
+
|
|
35
|
+
Task ID: ${task.id}
|
|
36
|
+
|
|
37
|
+
Context:
|
|
38
|
+
${context ? `Files: ${context.files.map((f) => "`" + f + "`").join(", ")} \nInstructions: ${context.instructions}` : "No pre-researched context provided."}
|
|
39
|
+
|
|
40
|
+
Notes:
|
|
41
|
+
${notes.length > 0 ? notes.map((n, i) => `${i + 1}. ${n}`).join("\n") : "None"}`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const fastMode = async (query: string) => {
|
|
45
|
+
const abortController = useFraudeStore.getState().abortController;
|
|
46
|
+
if (!abortController) {
|
|
47
|
+
throw new Error("No abort controller found");
|
|
48
|
+
}
|
|
49
|
+
const response = await getFastAgent().stream(query, {
|
|
50
|
+
abortSignal: abortController.signal,
|
|
51
|
+
});
|
|
52
|
+
checkAbort();
|
|
53
|
+
|
|
54
|
+
log("Fast Agent Response:");
|
|
55
|
+
log(JSON.stringify(response, null, 2));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const planMode = async (query: string) => {
|
|
59
|
+
const abortController = useFraudeStore.getState().abortController;
|
|
60
|
+
if (!abortController) {
|
|
61
|
+
throw new Error("No abort controller found");
|
|
62
|
+
}
|
|
63
|
+
const response = await getManagerAgent().stream(query, {
|
|
64
|
+
abortSignal: abortController.signal,
|
|
65
|
+
});
|
|
66
|
+
checkAbort();
|
|
67
|
+
|
|
68
|
+
log("Manager Response:");
|
|
69
|
+
log(JSON.stringify(response, null, 2));
|
|
70
|
+
|
|
71
|
+
let nextTask = await getNextTodo();
|
|
72
|
+
while (!nextTask.done && nextTask.task) {
|
|
73
|
+
checkAbort();
|
|
74
|
+
|
|
75
|
+
let taskContext = getTaskContext(nextTask.task);
|
|
76
|
+
updateOutput("log", "Working on task: " + nextTask.task.description);
|
|
77
|
+
const response = await getWorkerSubAgent().stream(taskContext, {
|
|
78
|
+
abortSignal: abortController.signal,
|
|
79
|
+
});
|
|
80
|
+
checkAbort();
|
|
81
|
+
|
|
82
|
+
log("Worker Response:");
|
|
83
|
+
log(JSON.stringify(response, null, 2));
|
|
84
|
+
const taskAfterWorker = await getTodoById(nextTask.task.id);
|
|
85
|
+
if (taskAfterWorker && taskAfterWorker.status === "in-progress") {
|
|
86
|
+
log(
|
|
87
|
+
"Worker finished but didn't update status. Auto-advancing to 'reviewing'.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const getUpdatedTask = await getTodoById(nextTask.task.id);
|
|
92
|
+
if (!getUpdatedTask) {
|
|
93
|
+
updateOutput("error", "Task not found");
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
taskContext = getTaskContext(getUpdatedTask);
|
|
97
|
+
updateOutput(
|
|
98
|
+
"log",
|
|
99
|
+
"Reviewing changes for task: " + nextTask.task.description,
|
|
100
|
+
);
|
|
101
|
+
const reviewResponse = await getReviewerSubAgent().stream(taskContext, {
|
|
102
|
+
abortSignal: abortController.signal,
|
|
103
|
+
});
|
|
104
|
+
checkAbort();
|
|
105
|
+
|
|
106
|
+
log("Review Response:");
|
|
107
|
+
log(JSON.stringify(reviewResponse, null, 2));
|
|
108
|
+
|
|
109
|
+
// SAFETY CHECK: Did the reviewer complete the task?
|
|
110
|
+
const postReviewTask = await getTodoById(nextTask.task.id);
|
|
111
|
+
if (
|
|
112
|
+
postReviewTask &&
|
|
113
|
+
postReviewTask.status !== "completed" &&
|
|
114
|
+
postReviewTask.status !== "pending"
|
|
115
|
+
) {
|
|
116
|
+
log(
|
|
117
|
+
"Warning: Reviewer did not complete or reject task. Auto-completing to proceed.",
|
|
118
|
+
);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
nextTask = await getNextTodo();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const askMode = async (query: string) => {
|
|
127
|
+
const abortController = useFraudeStore.getState().abortController;
|
|
128
|
+
if (!abortController) {
|
|
129
|
+
throw new Error("No abort controller found");
|
|
130
|
+
}
|
|
131
|
+
const response = await getAskAgent().stream(query, {
|
|
132
|
+
abortSignal: abortController.signal,
|
|
133
|
+
});
|
|
134
|
+
checkAbort();
|
|
135
|
+
|
|
136
|
+
log("Ask Agent Response:");
|
|
137
|
+
log(JSON.stringify(response, null, 2));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export default async function QueryHandler(query: string) {
|
|
141
|
+
if (query === "exit") {
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
updateOutput("command", query);
|
|
145
|
+
if (query.startsWith("/")) {
|
|
146
|
+
useFraudeStore.setState({
|
|
147
|
+
status: 2,
|
|
148
|
+
});
|
|
149
|
+
await CommandCenter.processCommand(query);
|
|
150
|
+
useFraudeStore.setState({
|
|
151
|
+
status: 0,
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
log(`User Query: ${query}`);
|
|
156
|
+
|
|
157
|
+
useFraudeStore.setState({
|
|
158
|
+
status: 1,
|
|
159
|
+
elapsedTime: 0,
|
|
160
|
+
lastBreak: 0,
|
|
161
|
+
abortController: new AbortController(),
|
|
162
|
+
statusText: "Pondering",
|
|
163
|
+
});
|
|
164
|
+
resetStreamState();
|
|
165
|
+
|
|
166
|
+
// Initialize cognition and inject relevant knowledge
|
|
167
|
+
const contextManager = useFraudeStore.getState().contextManager;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
// Prime context with project knowledge (once per session)
|
|
171
|
+
// await contextManager.primeWithKnowledge();
|
|
172
|
+
|
|
173
|
+
// Inject query-specific context via orchestrator
|
|
174
|
+
await contextManager.injectQueryContext(query);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
log(`Knowledge injection failed (non-fatal): ${e}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
useFraudeStore.setState({
|
|
181
|
+
researchCache: {},
|
|
182
|
+
});
|
|
183
|
+
if (useFraudeStore.getState().executionMode == 0) {
|
|
184
|
+
// Fast Mode
|
|
185
|
+
await fastMode(query);
|
|
186
|
+
} else if (useFraudeStore.getState().executionMode == 1) {
|
|
187
|
+
// Planning Mode
|
|
188
|
+
await planMode(query);
|
|
189
|
+
} else if (useFraudeStore.getState().executionMode == 2) {
|
|
190
|
+
// Ask Mode
|
|
191
|
+
await askMode(query);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Persist session learnings after successful completion
|
|
195
|
+
try {
|
|
196
|
+
await contextManager.persistSession();
|
|
197
|
+
} catch (e) {
|
|
198
|
+
log(`Session persistence failed (non-fatal): ${e}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (pendingChanges.hasChanges()) {
|
|
202
|
+
useFraudeStore.setState({ status: 3, statusText: "Reviewing Changes" });
|
|
203
|
+
updateOutput("confirmation", "");
|
|
204
|
+
} else {
|
|
205
|
+
updateOutput(
|
|
206
|
+
"done",
|
|
207
|
+
`Task Completed in ${(useFraudeStore.getState().elapsedTime / 10).toFixed(1)}s`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
} catch (e: any) {
|
|
211
|
+
if (e?.name === "AbortError" || e?.message === "Aborted") {
|
|
212
|
+
log("Query aborted by user");
|
|
213
|
+
} else {
|
|
214
|
+
log(`Error in query handler: ${e?.message}`);
|
|
215
|
+
throw e; // Re-throw non-abort errors
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
// Cleanup unless in reviewing mode
|
|
219
|
+
if (useFraudeStore.getState().status !== 3) {
|
|
220
|
+
useFraudeStore.setState({
|
|
221
|
+
status: 0,
|
|
222
|
+
abortController: null,
|
|
223
|
+
statusText: "",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import useFraudeStore from "@/store/useFraudeStore";
|
|
2
|
+
import type { Server } from "bun";
|
|
3
|
+
|
|
4
|
+
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
5
|
+
type Handler = (req: Request) => Response | Promise<Response>;
|
|
6
|
+
const { updateOutput } = useFraudeStore.getState();
|
|
7
|
+
|
|
8
|
+
interface Route {
|
|
9
|
+
method: HttpMethod;
|
|
10
|
+
path: string;
|
|
11
|
+
handler: Handler;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WebSocketHandler {
|
|
15
|
+
open?: (ws: any) => void;
|
|
16
|
+
message: (ws: any, message: string) => void;
|
|
17
|
+
close?: (ws: any) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class BunApiRouter {
|
|
21
|
+
private static routers = new Map<string, BunApiRouter>();
|
|
22
|
+
private static _shared: BunApiRouter;
|
|
23
|
+
|
|
24
|
+
public static get shared(): BunApiRouter {
|
|
25
|
+
if (!this._shared) {
|
|
26
|
+
this._shared = new BunApiRouter();
|
|
27
|
+
}
|
|
28
|
+
return this._shared;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public static getRouter(id: string): BunApiRouter | undefined {
|
|
32
|
+
return this.routers.get(id);
|
|
33
|
+
}
|
|
34
|
+
public id: string = crypto.randomUUID();
|
|
35
|
+
public port: number = 3000;
|
|
36
|
+
private routes: Route[] = [];
|
|
37
|
+
private wsRoutes: { path: string; handler: WebSocketHandler }[] = [];
|
|
38
|
+
private server: Server<any> | null = null;
|
|
39
|
+
private resolveServicePromise: (() => void) | null = null;
|
|
40
|
+
public onStop?: () => void;
|
|
41
|
+
|
|
42
|
+
public static stopRouter(id: string) {
|
|
43
|
+
const router = this.routers.get(id);
|
|
44
|
+
if (router) {
|
|
45
|
+
router.stop();
|
|
46
|
+
this.routers.delete(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a route pattern matches a given pathname and extract params
|
|
52
|
+
* Supports dynamic segments like :userId
|
|
53
|
+
*/
|
|
54
|
+
private getParams(
|
|
55
|
+
pattern: string,
|
|
56
|
+
pathname: string,
|
|
57
|
+
): Record<string, string> | null {
|
|
58
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
59
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
60
|
+
|
|
61
|
+
if (patternParts.length !== pathParts.length) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const params: Record<string, string> = {};
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
68
|
+
const patternPart = patternParts[i]!;
|
|
69
|
+
const pathPart = pathParts[i]!;
|
|
70
|
+
|
|
71
|
+
if (patternPart.startsWith(":")) {
|
|
72
|
+
const paramName = patternPart.slice(1);
|
|
73
|
+
params[paramName] = pathPart;
|
|
74
|
+
} else if (patternPart !== pathPart) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return params;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register a new endpoint
|
|
84
|
+
* @param method HTTP Method
|
|
85
|
+
* @param path URL path (e.g. "/api/v1/users" or "/users/:id")
|
|
86
|
+
* @param handler Function to handle the request
|
|
87
|
+
*/
|
|
88
|
+
public register(
|
|
89
|
+
method: HttpMethod,
|
|
90
|
+
path: string,
|
|
91
|
+
handler: (req: Request & { params: Record<string, string> }) => any,
|
|
92
|
+
) {
|
|
93
|
+
// Check for duplicates
|
|
94
|
+
const duplicateIndex = this.routes.findIndex(
|
|
95
|
+
(r) => r.method === method && r.path === path,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (duplicateIndex !== -1) {
|
|
99
|
+
// Overwrite instead of warning, to allow re-registration during development/re-runs
|
|
100
|
+
this.routes[duplicateIndex] = { method, path, handler: handler as any };
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.routes.push({ method, path, handler: handler as any });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Register a WebSocket endpoint
|
|
109
|
+
* @param path URL path
|
|
110
|
+
* @param handler WebSocket event handlers
|
|
111
|
+
*/
|
|
112
|
+
public registerWebSocket(path: string, handler: WebSocketHandler) {
|
|
113
|
+
this.wsRoutes.push({ path, handler });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Start the server and block until interrupted.
|
|
118
|
+
* @param port Port to listen on (default: 3000)
|
|
119
|
+
*/
|
|
120
|
+
public async serve(port: number = 3000): Promise<void> {
|
|
121
|
+
if (this.server) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Router ${this.id} is already serving on port ${this.port}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.port = port;
|
|
128
|
+
|
|
129
|
+
// Create a promise that we can manually resolve to "unblock" the caller
|
|
130
|
+
const servicePromise = new Promise<void>((resolve) => {
|
|
131
|
+
this.resolveServicePromise = resolve;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
this.server = Bun.serve<{
|
|
136
|
+
handler: WebSocketHandler;
|
|
137
|
+
params: Record<string, string>;
|
|
138
|
+
}>({
|
|
139
|
+
port,
|
|
140
|
+
idleTimeout: 180,
|
|
141
|
+
fetch: async (req, server) => {
|
|
142
|
+
const url = new URL(req.url);
|
|
143
|
+
|
|
144
|
+
// Check for WebSocket upgrade
|
|
145
|
+
let wsParams: Record<string, string> = {};
|
|
146
|
+
const wsRoute = this.wsRoutes.find((r) => {
|
|
147
|
+
const p = this.getParams(r.path, url.pathname);
|
|
148
|
+
if (p) {
|
|
149
|
+
wsParams = p;
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
wsRoute &&
|
|
157
|
+
server.upgrade(req, {
|
|
158
|
+
data: { handler: wsRoute.handler, params: wsParams },
|
|
159
|
+
})
|
|
160
|
+
) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// CORS support
|
|
165
|
+
const corsHeaders = {
|
|
166
|
+
"Access-Control-Allow-Origin": "*",
|
|
167
|
+
"Access-Control-Allow-Methods":
|
|
168
|
+
"GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
169
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (req.method === "OPTIONS") {
|
|
173
|
+
return new Response(null, { headers: corsHeaders });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Default health check
|
|
177
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
178
|
+
return new Response("OK", { status: 200, headers: corsHeaders });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Find matching route and extract params
|
|
182
|
+
let params: Record<string, string> = {};
|
|
183
|
+
const route = this.routes.find((r) => {
|
|
184
|
+
if (r.method !== req.method) return false;
|
|
185
|
+
const p = this.getParams(r.path, url.pathname);
|
|
186
|
+
if (p) {
|
|
187
|
+
params = p;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (route) {
|
|
194
|
+
try {
|
|
195
|
+
// Attach params to request object
|
|
196
|
+
(req as any).params = params;
|
|
197
|
+
const response = await route.handler(req);
|
|
198
|
+
// Append CORS headers to the handler's response
|
|
199
|
+
const newHeaders = new Headers(response.headers);
|
|
200
|
+
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
201
|
+
newHeaders.set(key, value);
|
|
202
|
+
});
|
|
203
|
+
return new Response(response.body, {
|
|
204
|
+
status: response.status,
|
|
205
|
+
statusText: response.statusText,
|
|
206
|
+
headers: newHeaders,
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error("Error in handler:", error);
|
|
210
|
+
return new Response("Internal Server Error", {
|
|
211
|
+
status: 500,
|
|
212
|
+
headers: corsHeaders,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return new Response("Not Found", {
|
|
218
|
+
status: 404,
|
|
219
|
+
headers: corsHeaders,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
websocket: {
|
|
223
|
+
open(ws) {
|
|
224
|
+
if (ws.data?.handler?.open) {
|
|
225
|
+
ws.data.handler.open(ws);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
message(ws, message) {
|
|
229
|
+
if (ws.data?.handler?.message) {
|
|
230
|
+
ws.data.handler.message(
|
|
231
|
+
ws,
|
|
232
|
+
typeof message === "string" ? message : message.toString(),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
close(ws) {
|
|
237
|
+
if (ws.data?.handler?.close) {
|
|
238
|
+
ws.data.handler.close(ws);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Register this router instance
|
|
245
|
+
BunApiRouter.routers.set(this.id, this);
|
|
246
|
+
|
|
247
|
+
// Output the interactive component
|
|
248
|
+
updateOutput("interactive-server", this.id);
|
|
249
|
+
|
|
250
|
+
// Block until stopped
|
|
251
|
+
await servicePromise;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
updateOutput("error", `Failed to start server: ${error}`);
|
|
254
|
+
throw error;
|
|
255
|
+
} finally {
|
|
256
|
+
// Cleanup
|
|
257
|
+
if (this.server) {
|
|
258
|
+
this.server.stop(true); // Forced stop
|
|
259
|
+
this.server = null;
|
|
260
|
+
}
|
|
261
|
+
this.resolveServicePromise = null;
|
|
262
|
+
BunApiRouter.routers.delete(this.id);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
public stop() {
|
|
267
|
+
if (this.resolveServicePromise) {
|
|
268
|
+
this.resolveServicePromise();
|
|
269
|
+
this.resolveServicePromise = null;
|
|
270
|
+
if (this.onStop) this.onStop();
|
|
271
|
+
} else if (this.server) {
|
|
272
|
+
// Fallback if promise was already resolved or cleared but server is somehow active
|
|
273
|
+
this.server.stop(true);
|
|
274
|
+
this.server = null;
|
|
275
|
+
if (this.onStop) this.onStop();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import useFraudeStore from "@/store/useFraudeStore";
|
|
2
|
+
import log from "./logger";
|
|
3
|
+
import { UpdateSettings } from "@/config/settings";
|
|
4
|
+
import type { TokenUsage } from "@/types/TokenUsage";
|
|
5
|
+
|
|
6
|
+
interface StreamState {
|
|
7
|
+
reasoningText: string;
|
|
8
|
+
agentText: string;
|
|
9
|
+
currentToolCallId: string | null;
|
|
10
|
+
toolCallTimestamps: Map<string, number>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const state: StreamState = {
|
|
14
|
+
reasoningText: "",
|
|
15
|
+
agentText: "",
|
|
16
|
+
currentToolCallId: null,
|
|
17
|
+
toolCallTimestamps: new Map(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function resetState() {
|
|
21
|
+
state.reasoningText = "";
|
|
22
|
+
state.agentText = "";
|
|
23
|
+
state.currentToolCallId = null;
|
|
24
|
+
state.toolCallTimestamps.clear();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatDuration(ms: number): number {
|
|
28
|
+
return ms / 1000;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function handleStreamChunk(chunk: Record<string, unknown>): TokenUsage {
|
|
32
|
+
const { updateOutput } = useFraudeStore.getState();
|
|
33
|
+
const store = useFraudeStore.getState();
|
|
34
|
+
|
|
35
|
+
// log(`Stream chunk: ${JSON.stringify(chunk)}`);
|
|
36
|
+
|
|
37
|
+
switch (chunk.type) {
|
|
38
|
+
case "start":
|
|
39
|
+
resetState();
|
|
40
|
+
break;
|
|
41
|
+
|
|
42
|
+
case "reasoning-start":
|
|
43
|
+
state.reasoningText = "";
|
|
44
|
+
useFraudeStore.setState({ lastBreak: store.elapsedTime });
|
|
45
|
+
updateOutput("reasoning", "", { dontOverride: true });
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case "reasoning-delta":
|
|
49
|
+
state.reasoningText += chunk.text as string;
|
|
50
|
+
updateOutput("reasoning", state.reasoningText);
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case "reasoning-end": {
|
|
54
|
+
const elapsed = store.elapsedTime - store.lastBreak;
|
|
55
|
+
const duration = formatDuration(elapsed * 100);
|
|
56
|
+
updateOutput("reasoning", `${state.reasoningText}`, { duration });
|
|
57
|
+
useFraudeStore.setState({ lastBreak: store.elapsedTime });
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case "text-delta":
|
|
62
|
+
const lastItem = store.outputItems[store.outputItems.length - 1];
|
|
63
|
+
if (lastItem?.type === "toolCall") {
|
|
64
|
+
state.agentText = "";
|
|
65
|
+
}
|
|
66
|
+
state.agentText += chunk.text as string;
|
|
67
|
+
updateOutput("agentText", state.agentText);
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case "finish-step": {
|
|
71
|
+
const finishReason = chunk.finishReason as string;
|
|
72
|
+
if (finishReason === "stop" && state.agentText) {
|
|
73
|
+
// Final text output is already displayed
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Safely extract usage data using AI SDK's normalized format or provider raw format
|
|
77
|
+
const usage = chunk.usage as
|
|
78
|
+
| Record<string, number | undefined>
|
|
79
|
+
| undefined;
|
|
80
|
+
|
|
81
|
+
if (usage) {
|
|
82
|
+
const promptTokens = usage.promptTokens ?? usage.inputTokens ?? 0;
|
|
83
|
+
const completionTokens =
|
|
84
|
+
usage.completionTokens ?? usage.outputTokens ?? 0;
|
|
85
|
+
const totalTokens =
|
|
86
|
+
usage.totalTokens ?? promptTokens + completionTokens;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
promptTokens,
|
|
90
|
+
completionTokens,
|
|
91
|
+
totalTokens,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Return zero usage if not available
|
|
96
|
+
return {
|
|
97
|
+
promptTokens: 0,
|
|
98
|
+
completionTokens: 0,
|
|
99
|
+
totalTokens: 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case "finish": {
|
|
104
|
+
// const elapsed = store.elapsedTime;
|
|
105
|
+
// updateOutput(
|
|
106
|
+
// "done",
|
|
107
|
+
// `Finished in ${formatDuration(elapsed * 100).toFixed(1)}s`,
|
|
108
|
+
// );
|
|
109
|
+
resetState();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case "error": {
|
|
114
|
+
const error = chunk.error as Error;
|
|
115
|
+
updateOutput("error", error.message);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
default:
|
|
120
|
+
// Ignore other chunk types (start-step, tool-input-*, etc.)
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
promptTokens: 0,
|
|
125
|
+
completionTokens: 0,
|
|
126
|
+
totalTokens: 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function resetStreamState() {
|
|
131
|
+
resetState();
|
|
132
|
+
}
|