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,229 @@
1
+ import { z } from "zod";
2
+ import log from "../utils/logger";
3
+ import { join } from "path";
4
+ import { rename, mkdir } from "fs/promises";
5
+ import { getConfigDir } from "../utils/paths";
6
+ import { ModelSchema, parseModelUniqueId } from "../types/Model";
7
+ import type { TokenUsage } from "@/types/TokenUsage";
8
+ import useSettingsStore from "@/store/useSettingsStore";
9
+ import { SettingsSchema, type Config } from "./schema";
10
+ import useFraudeStore from "@/store/useFraudeStore";
11
+
12
+ class Settings {
13
+ private static instance: Settings | null = null;
14
+ private settingsDir: string;
15
+ private settings: Config;
16
+ private writePromise: Promise<void> = Promise.resolve(); // Chain writes to prevent race conditions
17
+
18
+ private constructor(config: Config, configDir: string) {
19
+ this.settings = config;
20
+ log("Settings loaded:", JSON.stringify(this.settings, null, 2));
21
+ this.settingsDir = configDir;
22
+ }
23
+
24
+ /**
25
+ * Initialize the Settings singleton. Call this once at app startup.
26
+ */
27
+ static async init(): Promise<Settings> {
28
+ if (Settings.instance) {
29
+ return Settings.instance;
30
+ }
31
+
32
+ const configDir = getConfigDir("fraude-code");
33
+ const config = await Settings.loadFromDisk(configDir);
34
+ Settings.instance = new Settings(config, configDir);
35
+ return Settings.instance;
36
+ }
37
+
38
+ /**
39
+ * Get the Settings instance. Throws if init() hasn't been called.
40
+ */
41
+ static getInstance(): Settings {
42
+ if (!Settings.instance) {
43
+ throw new Error("Settings not initialized. Call Settings.init() first.");
44
+ }
45
+ return Settings.instance;
46
+ }
47
+
48
+ /**
49
+ * Get a specific setting value.
50
+ */
51
+ get<K extends keyof Config>(key: K): Config[K] {
52
+ return this.settings[key];
53
+ }
54
+
55
+ /**
56
+ * Get all settings.
57
+ */
58
+ getAll(): Config {
59
+ return { ...this.settings };
60
+ }
61
+
62
+ /**
63
+ * Update settings and persist to disk.
64
+ */
65
+ async set<K extends keyof Config>(key: K, value: Config[K]): Promise<void> {
66
+ this.settings[key] = value;
67
+ await this.saveToDisk();
68
+ }
69
+
70
+ /**
71
+ * Update multiple settings at once and persist to disk (single write).
72
+ */
73
+ async setMultiple(updates: Partial<Config>): Promise<void> {
74
+ for (const [key, value] of Object.entries(updates)) {
75
+ (this.settings as any)[key] = value;
76
+ }
77
+ await this.saveToDisk();
78
+ }
79
+
80
+ /**
81
+ * Load settings from disk (static, used during initialization).
82
+ */
83
+ private static async loadFromDisk(configDir: string): Promise<Config> {
84
+ try {
85
+ const settingsPath = join(configDir, "settings.json");
86
+ const file = Bun.file(settingsPath);
87
+
88
+ if (!(await file.exists())) {
89
+ return SettingsSchema.parse({}); // Return defaults
90
+ }
91
+
92
+ const rawData = await file.json();
93
+ const result = SettingsSchema.safeParse(rawData);
94
+
95
+ if (!result.success) {
96
+ console.error(
97
+ "Invalid settings found, attempting to merge with defaults:",
98
+ result.error.format(),
99
+ );
100
+
101
+ // Backup the invalid settings file just in case
102
+ try {
103
+ await Bun.write(
104
+ `${settingsPath}.bak`,
105
+ JSON.stringify(rawData, null, 2),
106
+ );
107
+ useFraudeStore
108
+ .getState()
109
+ .updateOutput(
110
+ "log",
111
+ `Backed up invalid settings to ${settingsPath}.bak`,
112
+ );
113
+ } catch (backupError) {
114
+ useFraudeStore
115
+ .getState()
116
+ .updateOutput("error", "Failed to backup settings:" + backupError);
117
+ }
118
+
119
+ // Return a merge of defaults and raw data to preserve what we can
120
+ // We ensure critical fields like 'models' are at least the right type
121
+ const defaults = SettingsSchema.parse({});
122
+ const merged = { ...defaults, ...rawData };
123
+
124
+ // Safety checks for critical types to prevent runtime crashes
125
+ if (!Array.isArray(merged.models)) merged.models = defaults.models;
126
+ if (!Array.isArray(merged.history)) merged.history = defaults.history;
127
+
128
+ return merged as Config;
129
+ }
130
+
131
+ return result.data;
132
+ } catch (e) {
133
+ useFraudeStore
134
+ .getState()
135
+ .updateOutput("error", "Error loading settings:" + e);
136
+ return SettingsSchema.parse({});
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Save current settings to disk atomically.
142
+ * Writes are chained to prevent race conditions.
143
+ */
144
+ private async saveToDisk(): Promise<void> {
145
+ // Chain this write after any pending write completes
146
+ const doWrite = async () => {
147
+ const settingsPath = join(this.settingsDir, "settings.json");
148
+ const tempPath = `${settingsPath}.tmp`;
149
+ const content = JSON.stringify(this.settings, null, 2);
150
+
151
+ // Ensure the directory exists
152
+ await mkdir(this.settingsDir, { recursive: true });
153
+
154
+ await Bun.write(tempPath, content);
155
+ await rename(tempPath, settingsPath);
156
+ };
157
+
158
+ // Queue this write after the previous one (even if it failed)
159
+ this.writePromise = this.writePromise.then(doWrite, doWrite);
160
+ await this.writePromise;
161
+ }
162
+ }
163
+
164
+ const UpdateSettings = async (updates: Partial<Config>) => {
165
+ await Settings.getInstance().setMultiple(updates);
166
+ useSettingsStore.setState({ ...updates });
167
+ };
168
+
169
+ const addHistory = async (value: string) => {
170
+ const history = Settings.getInstance().get("history");
171
+ if (value.trim().toLowerCase() != history[0]?.trim().toLowerCase()) {
172
+ const newHistory = [value, ...history].slice(0, 50);
173
+ await UpdateSettings({ history: newHistory });
174
+ }
175
+ };
176
+
177
+ /**
178
+ * Increment token usage for a specific model.
179
+ * @param modelIdentifier - The model identifier (can be unique ID "name|type" or just "name")
180
+ * @param usage - Token usage data with prompt, completion, and total counts
181
+ */
182
+ const incrementModelUsage = async (
183
+ modelIdentifier: string,
184
+ usage: TokenUsage,
185
+ ): Promise<void> => {
186
+ if (usage.totalTokens <= 0) return;
187
+
188
+ const settings = Settings.getInstance();
189
+ const models = [...settings.get("models")];
190
+
191
+ // Try parsing as unique ID (name|type format)
192
+ const parsed = parseModelUniqueId(modelIdentifier);
193
+ let modelIndex: number;
194
+
195
+ if (parsed) {
196
+ // Match by both name and type
197
+ modelIndex = models.findIndex(
198
+ (m) => m.name === parsed.name && m.type === parsed.type,
199
+ );
200
+ } else {
201
+ // Fall back to name-only matching (legacy format)
202
+ modelIndex = models.findIndex((m) => m.name === modelIdentifier);
203
+ }
204
+
205
+ if (modelIndex !== -1) {
206
+ const model = models[modelIndex]!;
207
+ models[modelIndex] = {
208
+ ...model,
209
+ usage: {
210
+ promptTokens: (model.usage?.promptTokens ?? 0) + usage.promptTokens,
211
+ completionTokens:
212
+ (model.usage?.completionTokens ?? 0) + usage.completionTokens,
213
+ totalTokens: (model.usage?.totalTokens ?? 0) + usage.totalTokens,
214
+ },
215
+ };
216
+ // log(JSON.stringify(models, null, 2));
217
+ await UpdateSettings({ models });
218
+ }
219
+ };
220
+
221
+ export default Settings;
222
+
223
+ export {
224
+ Settings,
225
+ type Config,
226
+ UpdateSettings,
227
+ addHistory,
228
+ incrementModelUsage,
229
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { render } from "ink";
4
+ import App from "./components/App";
5
+ import log, { resetLog } from "./utils/logger";
6
+ import { Settings } from "./config/settings";
7
+ import useSettingsStore from "./store/useSettingsStore";
8
+ import OllamaClient from "@/services/ollama";
9
+ import MistralClient from "@/services/mistral";
10
+ import CerebrasClient from "@/services/cerebras";
11
+ import GoogleClient from "@/services/google";
12
+ import CommandCenter from "@/commands";
13
+ import { getKnowledgeOrchestrator } from "@/services/knowledgeOrchestrator";
14
+
15
+ // Global error handlers to catch and suppress AbortErrors
16
+ process.on("unhandledRejection", (reason) => {
17
+ // Suppress AbortErrors - they're expected when cancelling operations
18
+ if (
19
+ reason instanceof Error &&
20
+ (reason.name === "AbortError" ||
21
+ reason.message === "The operation was aborted.")
22
+ ) {
23
+ return;
24
+ }
25
+ // For DOMException AbortError (code 20)
26
+ if (
27
+ reason &&
28
+ typeof reason === "object" &&
29
+ "code" in reason &&
30
+ reason.code === 20
31
+ ) {
32
+ return;
33
+ }
34
+ console.error("Unhandled rejection:", reason);
35
+ });
36
+
37
+ process.on("uncaughtException", (error) => {
38
+ // Suppress AbortErrors
39
+ if (
40
+ error.name === "AbortError" ||
41
+ error.message === "The operation was aborted."
42
+ ) {
43
+ return;
44
+ }
45
+ console.error("Uncaught exception:", error);
46
+ process.exit(1);
47
+ });
48
+
49
+ const syncModels = async () => {
50
+ await OllamaClient.syncOllamaModels();
51
+ await MistralClient.syncMistralModels();
52
+ await CerebrasClient.syncCerebrasModels();
53
+ await GoogleClient.syncGoogleModels();
54
+ };
55
+
56
+ async function main() {
57
+ resetLog();
58
+ console.clear();
59
+
60
+ try {
61
+ const envFile = Bun.file(".env");
62
+ if (await envFile.exists()) {
63
+ const envText = await envFile.text();
64
+ const lines = envText.split("\n");
65
+ for (const line of lines) {
66
+ const [key, ...valueParts] = line.trim().split("=");
67
+ if (key && !key.startsWith("#") && valueParts.length > 0) {
68
+ const value = valueParts.join("=").replace(/^["']|["']$/g, "");
69
+ process.env[key.trim()] = value;
70
+ log(`Loaded env var: ${key.trim()}`);
71
+ }
72
+ }
73
+ }
74
+ } catch (e) {
75
+ log(`Failed to load .env file: ${e}`);
76
+ }
77
+
78
+ await Settings.init();
79
+ useSettingsStore.getState().syncWithSettings();
80
+ await CommandCenter.loadPlugins();
81
+ syncModels();
82
+ const { waitUntilExit } = render(<App />, { exitOnCtrlC: false });
83
+
84
+ // Background index the project (fire-and-forget)
85
+ getKnowledgeOrchestrator()
86
+ .indexProject(process.cwd())
87
+ .catch((e: Error) => log(`Background indexing failed: ${e}`));
88
+
89
+ // Handle graceful exit
90
+ const exitHandler = () => {
91
+ process.exit(0);
92
+ };
93
+
94
+ process.on("SIGTERM", exitHandler);
95
+
96
+ await waitUntilExit();
97
+ process.exit(0);
98
+ }
99
+
100
+ main();
@@ -0,0 +1,71 @@
1
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
2
+ import { createGroq } from "@ai-sdk/groq";
3
+ import { createOllama } from "ollama-ai-provider-v2";
4
+ import { createCerebras } from "@ai-sdk/cerebras";
5
+ import { createMistral } from "@ai-sdk/mistral";
6
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
7
+ import useSettingsStore from "@/store/useSettingsStore";
8
+ import { parseModelUniqueId } from "@/types/Model";
9
+
10
+ const getSettings = () => useSettingsStore.getState();
11
+
12
+ /**
13
+ * Get the provider type and actual model name from a model identifier.
14
+ * Supports both:
15
+ * - Unique ID format: "modelName|type" (e.g., "openai/gpt-oss-120b|openrouter")
16
+ * - Legacy name-only format: "modelName" (e.g., "openai/gpt-oss-120b")
17
+ */
18
+ const getProviderForModel = (
19
+ modelIdentifier: string,
20
+ ): { name: string; type: string } => {
21
+ // Try parsing as unique ID (name|type format)
22
+ const parsed = parseModelUniqueId(modelIdentifier);
23
+ if (parsed) {
24
+ return parsed;
25
+ }
26
+
27
+ // Fall back to name-only lookup for backwards compatibility
28
+ const { models } = getSettings();
29
+ const model = models.find((m) => m.name === modelIdentifier);
30
+ if (!model) throw new Error(`Model not found: ${modelIdentifier}`);
31
+ return { name: model.name, type: model.type };
32
+ };
33
+
34
+ export function getModel(modelIdentifier: string) {
35
+ const { name, type } = getProviderForModel(modelIdentifier);
36
+ switch (type) {
37
+ case "groq":
38
+ const groq = createGroq({
39
+ apiKey: getSettings().groq_api_key || process.env.GROQ_API_KEY,
40
+ });
41
+ return groq(name);
42
+ case "ollama":
43
+ const ollama = createOllama({
44
+ baseURL: `${getSettings().ollamaUrl || process.env.OLLAMA_URL || "http://localhost:11434"}/api`,
45
+ });
46
+ return ollama(name);
47
+ case "openrouter":
48
+ const openrouter = createOpenRouter({
49
+ apiKey:
50
+ getSettings().openrouter_api_key || process.env.OPENROUTER_API_KEY,
51
+ });
52
+ return openrouter(name);
53
+ case "cerebras":
54
+ const cerebras = createCerebras({
55
+ apiKey: getSettings().cerebras_api_key || process.env.CEREBRAS_API_KEY,
56
+ });
57
+ return cerebras(name);
58
+ case "mistral":
59
+ const mistral = createMistral({
60
+ apiKey: getSettings().mistral_api_key || process.env.MISTRAL_API_KEY,
61
+ });
62
+ return mistral(name);
63
+ case "google":
64
+ const google = createGoogleGenerativeAI({
65
+ apiKey: getSettings().google_api_key || process.env.GOOGLE_API_KEY,
66
+ });
67
+ return google(name);
68
+ default:
69
+ throw new Error(`Unknown provider: ${type}`);
70
+ }
71
+ }
@@ -0,0 +1,123 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import type { Command } from "@/types/CommandDefinition";
5
+ import log from "@/utils/logger";
6
+ import { Settings, UpdateSettings } from "@/config/settings";
7
+ import { BunApiRouter } from "@/utils/router";
8
+ import useFraudeStore from "@/store/useFraudeStore";
9
+ import type { PluginContext } from "@/types/PluginContext";
10
+ import Agent from "@/agent/agent";
11
+
12
+ export class PluginLoader {
13
+ private pluginsDirs: string[];
14
+
15
+ constructor() {
16
+ // Get the system-wide config directory
17
+ const configDir = (Settings as any).instance
18
+ ? Settings.getInstance().getAll()
19
+ ? (Settings as any).instance.settingsDir
20
+ : ""
21
+ : "";
22
+
23
+ this.pluginsDirs = [
24
+ path.resolve(import.meta.dir, "../../plugins"), // Plugin folder in the source/package root
25
+ path.resolve(process.cwd(), ".fraude", "plugins"), // Local plugins in the current working directory
26
+ path.resolve(os.homedir(), "fraude", "plugins"), // Global plugins in the home directory
27
+ ];
28
+
29
+ if (configDir) {
30
+ this.pluginsDirs.push(path.join(configDir, "plugins")); // Global system config plugins
31
+ }
32
+
33
+ // Add any plugins folder relative to the executable if not already covered
34
+ const execPath = process.argv[1];
35
+ if (execPath) {
36
+ const execDir = path.dirname(execPath);
37
+ if (execDir !== process.cwd()) {
38
+ this.pluginsDirs.push(path.resolve(execDir, "./plugins"));
39
+ }
40
+ }
41
+
42
+ // De-duplicate paths
43
+ this.pluginsDirs = Array.from(new Set(this.pluginsDirs));
44
+ }
45
+
46
+ async loadPlugins(): Promise<Command[]> {
47
+ const allCommands: Command[] = [];
48
+ const loadedPluginNames = new Set<string>();
49
+
50
+ const context: PluginContext = {
51
+ log: log,
52
+ Router: BunApiRouter,
53
+ router: BunApiRouter.shared,
54
+ Agent: Agent,
55
+ settings: {
56
+ get: (key) => Settings.getInstance().get(key as any),
57
+ getAll: () => Settings.getInstance().getAll(),
58
+ update: async (updates) => {
59
+ await UpdateSettings(updates);
60
+ },
61
+ },
62
+ ui: {
63
+ updateOutput: (type, content) =>
64
+ useFraudeStore.getState().updateOutput(type, content),
65
+ },
66
+ utils: {},
67
+ };
68
+
69
+ for (const dir of this.pluginsDirs) {
70
+ try {
71
+ await fs.access(dir);
72
+ } catch {
73
+ continue;
74
+ }
75
+
76
+ log(`Searching for plugins in: ${dir}`);
77
+ const entries = await fs.readdir(dir, { withFileTypes: true });
78
+
79
+ for (const entry of entries) {
80
+ if (entry.isDirectory()) {
81
+ if (loadedPluginNames.has(entry.name)) continue;
82
+
83
+ const pluginPath = path.join(dir, entry.name);
84
+ const entryPoint = path.join(pluginPath, "index.ts");
85
+
86
+ try {
87
+ await fs.access(entryPoint);
88
+ const pluginModule = await import(entryPoint);
89
+
90
+ if (pluginModule.default) {
91
+ let commands: Command | Command[];
92
+
93
+ if (typeof pluginModule.default === "function") {
94
+ // Context-aware plugin
95
+ commands = await pluginModule.default(context);
96
+ } else {
97
+ // Legacy static object plugin
98
+ commands = pluginModule.default;
99
+ }
100
+
101
+ if (Array.isArray(commands)) {
102
+ allCommands.push(...commands);
103
+ } else {
104
+ allCommands.push(commands);
105
+ }
106
+ loadedPluginNames.add(entry.name);
107
+ log(`Loaded plugin: ${entry.name} from ${dir}`);
108
+ }
109
+ } catch (e) {
110
+ // Only log error if index.ts exists but failed to load
111
+ try {
112
+ await fs.access(entryPoint);
113
+ log(`Failed to load plugin ${entry.name}:`, e);
114
+ } catch {
115
+ // index.ts doesn't exist, ignore
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return allCommands;
122
+ }
123
+ }
@@ -0,0 +1,69 @@
1
+ import { UpdateSettings } from "@/config/settings";
2
+ import useSettingsStore from "../store/useSettingsStore";
3
+ import type { Model } from "../types/Model";
4
+
5
+ const getSettings = () => useSettingsStore.getState();
6
+
7
+ interface CerebrasModelsResponse {
8
+ object: string;
9
+ data: CerebrasModel[];
10
+ }
11
+
12
+ interface CerebrasModel {
13
+ id: string;
14
+ object: string;
15
+ created: number;
16
+ owned_by: string;
17
+ }
18
+
19
+ class CerebrasClient {
20
+ async syncCerebrasModels() {
21
+ const apiKey =
22
+ getSettings().cerebras_api_key || process.env.CEREBRAS_API_KEY;
23
+ if (!apiKey) {
24
+ return;
25
+ }
26
+ const url = `https://api.cerebras.ai/v1/models`;
27
+ const options = {
28
+ method: "GET",
29
+ headers: { Authorization: `Bearer ${apiKey}` },
30
+ };
31
+ const response = await fetch(url, options);
32
+ if (!response.ok) {
33
+ throw new Error(`Failed to fetch Cerebras models ${response.status}`);
34
+ }
35
+ const data = (await response.json()) as CerebrasModelsResponse;
36
+ const savedModels = getSettings().models;
37
+ const existingCerebrasModels = savedModels.filter(
38
+ (m) => m.type === "cerebras",
39
+ );
40
+ const otherModels = savedModels.filter((m) => m.type !== "cerebras");
41
+
42
+ const models: Model[] = data.data.map((model) => {
43
+ const existing = existingCerebrasModels.find((m) => m.name === model.id);
44
+ return {
45
+ type: "cerebras",
46
+ name: model.id,
47
+ modified_at: new Date(model.created * 1000).toISOString(),
48
+ digest: existing?.digest || "",
49
+ usage: existing?.usage || {
50
+ promptTokens: 0,
51
+ completionTokens: 0,
52
+ totalTokens: 0,
53
+ },
54
+ details: {
55
+ provider: "cerebras",
56
+ ...existing?.details,
57
+ },
58
+ } as Model;
59
+ });
60
+
61
+ // Merge: keep all other models, and replace/add the synced cerebras models
62
+ // Note: This effectively removes local cerebras models that are no longer returned by the API.
63
+ // If we want to keep them, we should change logic. But usually sync reflects source of truth.
64
+ const mergedModels = [...otherModels, ...models];
65
+ await UpdateSettings({ models: mergedModels });
66
+ }
67
+ }
68
+
69
+ export default new CerebrasClient();