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,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();
|
|
Binary file
|
|
@@ -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();
|