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.
Files changed (127) hide show
  1. package/README.md +68 -0
  2. package/dist/index.js +179297 -0
  3. package/package.json +88 -0
  4. package/src/agent/agent.ts +475 -0
  5. package/src/agent/contextManager.ts +141 -0
  6. package/src/agent/index.ts +14 -0
  7. package/src/agent/pendingChanges.ts +270 -0
  8. package/src/agent/prompts/AskPrompt.txt +10 -0
  9. package/src/agent/prompts/FastPrompt.txt +40 -0
  10. package/src/agent/prompts/PlannerPrompt.txt +51 -0
  11. package/src/agent/prompts/ReviewerPrompt.txt +57 -0
  12. package/src/agent/prompts/WorkerPrompt.txt +33 -0
  13. package/src/agent/subagents/askAgent.ts +37 -0
  14. package/src/agent/subagents/extractionAgent.ts +123 -0
  15. package/src/agent/subagents/fastAgent.ts +45 -0
  16. package/src/agent/subagents/managerAgent.ts +36 -0
  17. package/src/agent/subagents/relationAgent.ts +76 -0
  18. package/src/agent/subagents/researchSubAgent.ts +79 -0
  19. package/src/agent/subagents/reviewerSubAgent.ts +42 -0
  20. package/src/agent/subagents/workerSubAgent.ts +42 -0
  21. package/src/agent/tools/bashTool.ts +94 -0
  22. package/src/agent/tools/descriptions/bash.txt +47 -0
  23. package/src/agent/tools/descriptions/edit.txt +7 -0
  24. package/src/agent/tools/descriptions/glob.txt +4 -0
  25. package/src/agent/tools/descriptions/grep.txt +8 -0
  26. package/src/agent/tools/descriptions/lsp.txt +20 -0
  27. package/src/agent/tools/descriptions/plan.txt +3 -0
  28. package/src/agent/tools/descriptions/read.txt +9 -0
  29. package/src/agent/tools/descriptions/todo.txt +12 -0
  30. package/src/agent/tools/descriptions/write.txt +8 -0
  31. package/src/agent/tools/editTool.ts +44 -0
  32. package/src/agent/tools/globTool.ts +59 -0
  33. package/src/agent/tools/grepTool.ts +343 -0
  34. package/src/agent/tools/lspTool.ts +429 -0
  35. package/src/agent/tools/planTool.ts +118 -0
  36. package/src/agent/tools/readTool.ts +78 -0
  37. package/src/agent/tools/rememberTool.ts +91 -0
  38. package/src/agent/tools/testRunnerTool.ts +77 -0
  39. package/src/agent/tools/testTool.ts +44 -0
  40. package/src/agent/tools/todoTool.ts +224 -0
  41. package/src/agent/tools/writeTool.ts +33 -0
  42. package/src/commands/COMMANDS.ts +38 -0
  43. package/src/commands/cerebras/auth.ts +27 -0
  44. package/src/commands/cerebras/index.ts +31 -0
  45. package/src/commands/forget.ts +29 -0
  46. package/src/commands/google/auth.ts +24 -0
  47. package/src/commands/google/index.ts +31 -0
  48. package/src/commands/groq/add_model.ts +60 -0
  49. package/src/commands/groq/auth.ts +24 -0
  50. package/src/commands/groq/index.ts +33 -0
  51. package/src/commands/index.ts +65 -0
  52. package/src/commands/knowledge.ts +92 -0
  53. package/src/commands/log.ts +32 -0
  54. package/src/commands/mistral/auth.ts +27 -0
  55. package/src/commands/mistral/index.ts +31 -0
  56. package/src/commands/model/index.ts +145 -0
  57. package/src/commands/models/index.ts +16 -0
  58. package/src/commands/ollama/index.ts +29 -0
  59. package/src/commands/openrouter/add_model.ts +64 -0
  60. package/src/commands/openrouter/auth.ts +24 -0
  61. package/src/commands/openrouter/index.ts +33 -0
  62. package/src/commands/remember.ts +48 -0
  63. package/src/commands/serve.ts +31 -0
  64. package/src/commands/session/index.ts +21 -0
  65. package/src/commands/usage.ts +15 -0
  66. package/src/commands/visualize.ts +773 -0
  67. package/src/components/App.tsx +55 -0
  68. package/src/components/IntroComponent.tsx +70 -0
  69. package/src/components/LoaderComponent.tsx +68 -0
  70. package/src/components/OutputRenderer.tsx +88 -0
  71. package/src/components/SettingsRenderer.tsx +23 -0
  72. package/src/components/input/CommandSuggestions.tsx +41 -0
  73. package/src/components/input/FileSuggestions.tsx +61 -0
  74. package/src/components/input/InputBox.tsx +371 -0
  75. package/src/components/output/CheckpointView.tsx +13 -0
  76. package/src/components/output/CommandView.tsx +13 -0
  77. package/src/components/output/CommentView.tsx +12 -0
  78. package/src/components/output/ConfirmationView.tsx +179 -0
  79. package/src/components/output/ContextUsage.tsx +62 -0
  80. package/src/components/output/DiffView.tsx +202 -0
  81. package/src/components/output/ErrorView.tsx +14 -0
  82. package/src/components/output/InteractiveServerView.tsx +69 -0
  83. package/src/components/output/KnowledgeView.tsx +220 -0
  84. package/src/components/output/MarkdownView.tsx +15 -0
  85. package/src/components/output/ModelSelectView.tsx +71 -0
  86. package/src/components/output/ReasoningView.tsx +21 -0
  87. package/src/components/output/ToolCallView.tsx +45 -0
  88. package/src/components/settings/ModelList.tsx +250 -0
  89. package/src/components/settings/TokenUsage.tsx +274 -0
  90. package/src/config/schema.ts +19 -0
  91. package/src/config/settings.ts +229 -0
  92. package/src/index.tsx +100 -0
  93. package/src/parsers/tree-sitter-python.wasm +0 -0
  94. package/src/providers/providers.ts +71 -0
  95. package/src/services/PluginLoader.ts +123 -0
  96. package/src/services/cerebras.ts +69 -0
  97. package/src/services/embeddingService.ts +229 -0
  98. package/src/services/google.ts +65 -0
  99. package/src/services/graphSerializer.ts +248 -0
  100. package/src/services/groq.ts +23 -0
  101. package/src/services/knowledgeOrchestrator.ts +286 -0
  102. package/src/services/mistral.ts +79 -0
  103. package/src/services/ollama.ts +109 -0
  104. package/src/services/openrouter.ts +23 -0
  105. package/src/services/symbolExtractor.ts +277 -0
  106. package/src/store/useFraudeStore.ts +123 -0
  107. package/src/store/useSettingsStore.ts +38 -0
  108. package/src/theme.ts +26 -0
  109. package/src/types/Agent.ts +147 -0
  110. package/src/types/CommandDefinition.ts +8 -0
  111. package/src/types/Model.ts +94 -0
  112. package/src/types/OutputItem.ts +24 -0
  113. package/src/types/PluginContext.ts +55 -0
  114. package/src/types/TokenUsage.ts +5 -0
  115. package/src/types/assets.d.ts +4 -0
  116. package/src/utils/agentCognition.ts +1152 -0
  117. package/src/utils/fileSuggestions.ts +111 -0
  118. package/src/utils/index.ts +17 -0
  119. package/src/utils/initFraude.ts +8 -0
  120. package/src/utils/logger.ts +24 -0
  121. package/src/utils/lspClient.ts +1415 -0
  122. package/src/utils/paths.ts +24 -0
  123. package/src/utils/queryHandler.ts +227 -0
  124. package/src/utils/router.ts +278 -0
  125. package/src/utils/streamHandler.ts +132 -0
  126. package/src/utils/treeSitterQueries.ts +125 -0
  127. 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
+ }