woopcode 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/CONTRIBUTING.md +329 -0
- package/LICENSE +21 -0
- package/README.md +582 -0
- package/cli.ts +17 -0
- package/commands/agent.tsx +139 -0
- package/commands/agentController.ts +138 -0
- package/commands/models.ts +21 -0
- package/commands/providers/index.ts +12 -0
- package/commands/providers/listProviders.ts +28 -0
- package/commands/providers/login.ts +28 -0
- package/commands/providers/logout.ts +33 -0
- package/commands/providers/setProvider.ts +34 -0
- package/config/authProvider.ts +54 -0
- package/config/client.ts +163 -0
- package/config/config.ts +118 -0
- package/config/conversation.json +1 -0
- package/config/models.json +50 -0
- package/config/paths.ts +96 -0
- package/config/providers.json +21 -0
- package/config/runtime.ts +130 -0
- package/config/systemPrompt.ts +54 -0
- package/config/types.ts +88 -0
- package/onboarding/FLOW.md +291 -0
- package/onboarding/index.ts +56 -0
- package/onboarding/providers.ts +47 -0
- package/onboarding/setupWizard.tsx +193 -0
- package/onboarding/test-reset.ts +55 -0
- package/package.json +86 -0
- package/tools/createFile.ts +24 -0
- package/tools/editFile.ts +85 -0
- package/tools/findFiles.ts +94 -0
- package/tools/grep.ts +68 -0
- package/tools/index.ts +26 -0
- package/tools/listFiles.ts +49 -0
- package/tools/readFile.ts +53 -0
- package/tools/runTests.ts +29 -0
- package/tools/terminal.ts +37 -0
- package/tools/writeFile.ts +75 -0
- package/tui/package.json +0 -0
- package/tui/src/app.tsx +60 -0
- package/tui/src/components/ApprovalFooter.tsx +29 -0
- package/tui/src/components/AsciiLogo.tsx +78 -0
- package/tui/src/components/BootScreen.tsx +76 -0
- package/tui/src/components/CapabilityRow.tsx +17 -0
- package/tui/src/components/CodeBlock.tsx +70 -0
- package/tui/src/components/DiffPreview.tsx +46 -0
- package/tui/src/components/DiffViewer.tsx +36 -0
- package/tui/src/components/HomeFooter.tsx +11 -0
- package/tui/src/components/HomeScreen.tsx +71 -0
- package/tui/src/components/InlineCode.tsx +13 -0
- package/tui/src/components/LogoReveal.tsx +158 -0
- package/tui/src/components/Markdown.tsx +280 -0
- package/tui/src/components/MessageRenderer.tsx +84 -0
- package/tui/src/components/PromptCard.tsx +52 -0
- package/tui/src/components/StreamingCursor.tsx +13 -0
- package/tui/src/components/ThinkingIndicator.tsx +14 -0
- package/tui/src/components/ToolStatus.tsx +26 -0
- package/tui/src/header.tsx +25 -0
- package/tui/src/hooks/useBootAnimation.ts +67 -0
- package/tui/src/hooks/useLogoAnimation.ts +114 -0
- package/tui/src/index.ts +5 -0
- package/tui/src/prompt.tsx +66 -0
- package/tui/src/statusBar.tsx +83 -0
- package/tui/src/store/ui-store.ts +234 -0
- package/tui/src/store/useUIStore.ts +9 -0
- package/tui/src/timeline.tsx +96 -0
- package/tui/src/types.ts +40 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import {
|
|
3
|
+
buildRepositoryContext,
|
|
4
|
+
getConfig,
|
|
5
|
+
getConversation,
|
|
6
|
+
saveConversation,
|
|
7
|
+
} from "../config/config";
|
|
8
|
+
import { createProviderClient } from "../config/client";
|
|
9
|
+
import type { Message } from "../config/types";
|
|
10
|
+
import type { AgentCallbacks } from "../config/types";
|
|
11
|
+
import { agentLoop } from "../config/runtime";
|
|
12
|
+
import { App, store } from "../tui/src";
|
|
13
|
+
import { render } from "ink";
|
|
14
|
+
import { AgentController } from "./agentController";
|
|
15
|
+
import { ACTIVE_PROVIDER_MODELS } from "../config/client";
|
|
16
|
+
import type { HomeScreenData } from "../tui/src/components/HomeScreen";
|
|
17
|
+
import { ensureProviderConfigured } from "../onboarding";
|
|
18
|
+
|
|
19
|
+
export const agentCommand = new Command("agent")
|
|
20
|
+
.description("Runs the agent")
|
|
21
|
+
.option("-p, --prompt <prompt>", "prompt", "")
|
|
22
|
+
.action(runAgent);
|
|
23
|
+
|
|
24
|
+
/** Runs the interactive agent from either `woopcode` or `woopcode agent`. */
|
|
25
|
+
export async function runAgent() {
|
|
26
|
+
// Ensure provider is configured (launches onboarding if needed)
|
|
27
|
+
await ensureProviderConfigured();
|
|
28
|
+
|
|
29
|
+
let cancelStatusTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
30
|
+
const config = await getConfig();
|
|
31
|
+
const provider = config.defaultProvider;
|
|
32
|
+
const apiKey = config.providers[provider].apiKey;
|
|
33
|
+
|
|
34
|
+
const callbacks: AgentCallbacks = {
|
|
35
|
+
onStatus(status) {
|
|
36
|
+
if (cancelStatusTimeout) {
|
|
37
|
+
clearTimeout(cancelStatusTimeout);
|
|
38
|
+
cancelStatusTimeout = undefined;
|
|
39
|
+
}
|
|
40
|
+
store.setStatus(status);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
onToolStart(tool) {
|
|
44
|
+
store.finishAssistantMessage();
|
|
45
|
+
store.startTool(tool);
|
|
46
|
+
store.setStatus(`Running ${tool.name}...`);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
onToolFinish(tool) {
|
|
50
|
+
store.finishTool(tool.id);
|
|
51
|
+
store.setStatus("Thinking...");
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
onText(text) {
|
|
55
|
+
store.appendAssistantText(text);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
onDone() {
|
|
59
|
+
//console.log("onDone received");
|
|
60
|
+
store.finishAssistantMessage();
|
|
61
|
+
store.setStatus("Ready");
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
onError(error) {
|
|
65
|
+
store.finishAssistantMessage();
|
|
66
|
+
store.setStatus(`Error: ${error.message}`);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
onCancel() {
|
|
70
|
+
store.finishAssistantMessage();
|
|
71
|
+
store.setStatus("Cancelled");
|
|
72
|
+
|
|
73
|
+
cancelStatusTimeout = setTimeout(() => {
|
|
74
|
+
store.setStatus("Ready");
|
|
75
|
+
}, 1000);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const controller = new AgentController(provider, apiKey, callbacks);
|
|
79
|
+
await controller.initialize();
|
|
80
|
+
const homeScreen = await buildHomeScreen(provider);
|
|
81
|
+
|
|
82
|
+
const { unmount } = render(
|
|
83
|
+
<App controller={controller} onExit={handleExit} homeScreen={homeScreen} />,
|
|
84
|
+
{ exitOnCtrlC: false },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
let exiting = false;
|
|
88
|
+
|
|
89
|
+
async function handleExit() {
|
|
90
|
+
if (exiting) return;
|
|
91
|
+
exiting = true;
|
|
92
|
+
|
|
93
|
+
controller.cancel();
|
|
94
|
+
await controller.dispose();
|
|
95
|
+
unmount();
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
process.once("SIGINT", () => {
|
|
100
|
+
void handleExit();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function buildHomeScreen(provider: string): Promise<HomeScreenData> {
|
|
105
|
+
const repository = process.cwd().split("/").filter(Boolean).at(-1) ?? "workspace";
|
|
106
|
+
const branch = await getBranch();
|
|
107
|
+
const providerLabel = provider === "google" ? "Gemini" : titleCase(provider);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
logoWord: "WOOPCODE",
|
|
111
|
+
subtitle: "AI software engineering agent",
|
|
112
|
+
promptExamples: [
|
|
113
|
+
"Explain this repository",
|
|
114
|
+
"Review recent changes",
|
|
115
|
+
"Find duplicate code",
|
|
116
|
+
"Generate tests",
|
|
117
|
+
"Optimize performance",
|
|
118
|
+
"Add authentication",
|
|
119
|
+
],
|
|
120
|
+
capabilities: ["Build", "Review", "Explain", "Refactor", "Debug", "Test", "Document"],
|
|
121
|
+
repository,
|
|
122
|
+
branch,
|
|
123
|
+
providerName: providerLabel,
|
|
124
|
+
provider: ACTIVE_PROVIDER_MODELS[provider] ?? providerLabel,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function getBranch(): Promise<string> {
|
|
129
|
+
try {
|
|
130
|
+
const branch = (await Bun.$`git branch --show-current`.text()).trim();
|
|
131
|
+
return branch || "detached";
|
|
132
|
+
} catch {
|
|
133
|
+
return "not a git repository";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function titleCase(value: string) {
|
|
138
|
+
return value.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
139
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createProviderClient } from "../config/client";
|
|
2
|
+
import {
|
|
3
|
+
buildRepositoryContext,
|
|
4
|
+
getConversation,
|
|
5
|
+
saveConversation,
|
|
6
|
+
} from "../config/config";
|
|
7
|
+
import { agentLoop } from "../config/runtime";
|
|
8
|
+
import type { AgentCallbacks, Message } from "../config/types";
|
|
9
|
+
import { store } from "../tui/src";
|
|
10
|
+
|
|
11
|
+
export class AgentController {
|
|
12
|
+
private conversation: Message[] = [];
|
|
13
|
+
private repoContext = "";
|
|
14
|
+
private pendingAssistantText: string | null = null;
|
|
15
|
+
private pendingUserMessage: Extract<Message, { role: "user" }> | null = null;
|
|
16
|
+
private abortController: AbortController | null = null;
|
|
17
|
+
private isRunning = false;
|
|
18
|
+
private wasCancelled = false;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly provider: string,
|
|
22
|
+
private readonly apiKey: string,
|
|
23
|
+
private readonly callbacks: AgentCallbacks,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
async run(prompt: string) {
|
|
27
|
+
if (this.isRunning) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.isRunning = true;
|
|
32
|
+
this.abortController = new AbortController();
|
|
33
|
+
this.wasCancelled = false;
|
|
34
|
+
|
|
35
|
+
const userMessage: Extract<Message, { role: "user" }> = {
|
|
36
|
+
role: "user",
|
|
37
|
+
content: prompt,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this.conversation.push(userMessage);
|
|
41
|
+
this.pendingUserMessage = userMessage;
|
|
42
|
+
|
|
43
|
+
const conversation = [...this.conversation];
|
|
44
|
+
this.pendingAssistantText = "";
|
|
45
|
+
|
|
46
|
+
// Update UI before starting the agent
|
|
47
|
+
store.addUserMessage(prompt);
|
|
48
|
+
store.setStatus("Thinking...");
|
|
49
|
+
|
|
50
|
+
const client = createProviderClient(this.provider, this.apiKey);
|
|
51
|
+
|
|
52
|
+
let response = "";
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
response = await agentLoop(
|
|
56
|
+
client,
|
|
57
|
+
conversation,
|
|
58
|
+
this.repoContext,
|
|
59
|
+
{
|
|
60
|
+
...this.callbacks,
|
|
61
|
+
onText: (text) => {
|
|
62
|
+
this.pendingAssistantText += text;
|
|
63
|
+
this.callbacks.onText?.(text);
|
|
64
|
+
},
|
|
65
|
+
onCancel: () => {
|
|
66
|
+
this.wasCancelled = true;
|
|
67
|
+
this.callbacks.onCancel?.();
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
this.abortController.signal,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const assistantText = response || this.pendingAssistantText;
|
|
74
|
+
|
|
75
|
+
if (!this.wasCancelled && assistantText?.trim()) {
|
|
76
|
+
this.conversation.push({
|
|
77
|
+
role: "assistant",
|
|
78
|
+
content: assistantText,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
this.abortController = null;
|
|
83
|
+
this.isRunning = false;
|
|
84
|
+
}
|
|
85
|
+
this.pendingAssistantText = null;
|
|
86
|
+
if (this.wasCancelled) {
|
|
87
|
+
this.removePendingUserMessage();
|
|
88
|
+
} else {
|
|
89
|
+
this.pendingUserMessage = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async initialize() {
|
|
96
|
+
this.conversation = await getConversation();
|
|
97
|
+
this.repoContext = await buildRepositoryContext();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async dispose() {
|
|
101
|
+
if (this.wasCancelled) {
|
|
102
|
+
this.pendingAssistantText = null;
|
|
103
|
+
this.removePendingUserMessage();
|
|
104
|
+
} else if (this.pendingAssistantText?.trim()) {
|
|
105
|
+
this.conversation.push({
|
|
106
|
+
role: "assistant",
|
|
107
|
+
content: this.pendingAssistantText,
|
|
108
|
+
});
|
|
109
|
+
this.pendingAssistantText = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await saveConversation(this.conversation);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
cancel() {
|
|
116
|
+
if (!this.isRunning) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.wasCancelled = true;
|
|
121
|
+
this.abortController?.abort();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
isBusy() {
|
|
125
|
+
return this.isRunning;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private removePendingUserMessage() {
|
|
129
|
+
if (!this.pendingUserMessage) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.conversation = this.conversation.filter(
|
|
134
|
+
(message) => message !== this.pendingUserMessage,
|
|
135
|
+
);
|
|
136
|
+
this.pendingUserMessage = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import models from "../config/models.json";
|
|
3
|
+
|
|
4
|
+
export const modelsCommand = new Command("models")
|
|
5
|
+
.description("Returns all the supported models")
|
|
6
|
+
.option("-m, --model <modelName>", "name of the model", "all")
|
|
7
|
+
.action((options) => {
|
|
8
|
+
if (options.model === "all") {
|
|
9
|
+
console.table(models);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const model = models.find((m) => m.id === options.model);
|
|
14
|
+
|
|
15
|
+
if (!model) {
|
|
16
|
+
console.error(`Model "${options.model}" not found.`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
console.table([model]);
|
|
20
|
+
console.log(options);
|
|
21
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { loginCommand } from "./login";
|
|
3
|
+
import { logoutCommand } from "./logout";
|
|
4
|
+
import { setProviderCommand } from "./setProvider";
|
|
5
|
+
import { listCommand } from "./listProviders";
|
|
6
|
+
|
|
7
|
+
export const providerCommand = new Command("providers")
|
|
8
|
+
.description("Provider related information")
|
|
9
|
+
.addCommand(loginCommand)
|
|
10
|
+
.addCommand(logoutCommand)
|
|
11
|
+
.addCommand(setProviderCommand)
|
|
12
|
+
.addCommand(listCommand);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "../../config/config";
|
|
3
|
+
|
|
4
|
+
export const listCommand = new Command("list")
|
|
5
|
+
.description("Returns all the Providers with their default & auth status)")
|
|
6
|
+
.option(
|
|
7
|
+
"-p, --provider <providerName>",
|
|
8
|
+
"Name of the provider (gemini, claude etc)",
|
|
9
|
+
"",
|
|
10
|
+
)
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const config = await getConfig();
|
|
13
|
+
|
|
14
|
+
const rows = Object.entries(config.providers).map(
|
|
15
|
+
([provider, details]: [string, any]) => {
|
|
16
|
+
const loggedIn = !!details.apiKey;
|
|
17
|
+
const isDefault = config.defaultProvider === provider;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
provider,
|
|
21
|
+
status: loggedIn ? "Logged in" : "Not Logged in",
|
|
22
|
+
default: isDefault ? "✔︎" : "",
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
console.table(rows);
|
|
28
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { loginProvider } from "../../config/authProvider";
|
|
3
|
+
import { getConfig, saveConfig } from "../../config/config";
|
|
4
|
+
|
|
5
|
+
export const loginCommand = new Command("login")
|
|
6
|
+
.description("Lets user login into the provider (use it as default)")
|
|
7
|
+
.option(
|
|
8
|
+
"-p, --provider <providerName>",
|
|
9
|
+
"Name of the provider (gemini, claude etc)",
|
|
10
|
+
"",
|
|
11
|
+
)
|
|
12
|
+
.option("-a, --api-key <apiKey>", "Your api key", "")
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const success = await loginProvider(options.provider, options.apiKey);
|
|
15
|
+
|
|
16
|
+
if (!success) {
|
|
17
|
+
console.error(" Invalid API key");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const config = await getConfig();
|
|
22
|
+
|
|
23
|
+
config.defaultProvider = options.provider;
|
|
24
|
+
config.providers[options.provider].apiKey = options.apiKey;
|
|
25
|
+
await saveConfig(config);
|
|
26
|
+
|
|
27
|
+
console.log("logging into " + options.provider);
|
|
28
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig, saveConfig } from "../../config/config";
|
|
3
|
+
|
|
4
|
+
export const logoutCommand = new Command("logout")
|
|
5
|
+
.description("Lets user logout from the provider")
|
|
6
|
+
.option(
|
|
7
|
+
"-p, --provider <providerName>",
|
|
8
|
+
"Name of the provider (gemini, claude etc)",
|
|
9
|
+
"",
|
|
10
|
+
)
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
const config = await getConfig();
|
|
13
|
+
|
|
14
|
+
if (!config.providers[options.provider]) {
|
|
15
|
+
console.error(`Unknown Provider ${options.provider}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!config.providers[options.provider].apiKey) {
|
|
20
|
+
console.error(`${options.provider} is not logged in`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
delete config.providers[options.provider].apiKey;
|
|
25
|
+
|
|
26
|
+
if (config.defaultProvider === options.provider) {
|
|
27
|
+
config.defaultProviders === "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await saveConfig(config);
|
|
31
|
+
|
|
32
|
+
console.log("logging out for provider " + options.provider);
|
|
33
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig, saveConfig } from "../../config/config";
|
|
3
|
+
|
|
4
|
+
export const setProviderCommand = new Command("set")
|
|
5
|
+
.description("Lets user set the default provider")
|
|
6
|
+
.option(
|
|
7
|
+
"-p, --provider <providerName>",
|
|
8
|
+
"Name of the provider (gemini, claude etc)",
|
|
9
|
+
"",
|
|
10
|
+
)
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
const config = await getConfig();
|
|
13
|
+
console.log("provider is " + JSON.stringify(options));
|
|
14
|
+
|
|
15
|
+
if (!config.providers[options.provider]) {
|
|
16
|
+
console.error(`${options.provider} does not exist`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!config.providers[options.provider].apiKey) {
|
|
21
|
+
console.error(`${options.provider} is not logged in`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (config.defaultProvider === options.provider) {
|
|
26
|
+
console.log(`${options.provider} is already default provider`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
config.defaultProvider = options.provider;
|
|
31
|
+
|
|
32
|
+
await saveConfig(config);
|
|
33
|
+
console.log(`Default provider set to ${options.provider}`);
|
|
34
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { fetch } from "bun";
|
|
2
|
+
|
|
3
|
+
export async function loginProvider(provider: string, apiKey: string) {
|
|
4
|
+
switch (provider) {
|
|
5
|
+
case "google":
|
|
6
|
+
return verifyGemini(apiKey);
|
|
7
|
+
case "groq":
|
|
8
|
+
return verifyGroq(apiKey);
|
|
9
|
+
case "openai":
|
|
10
|
+
return verifyOpenai(apiKey);
|
|
11
|
+
case "anthropic":
|
|
12
|
+
return verifyAnthropic(apiKey);
|
|
13
|
+
default:
|
|
14
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function verifyGemini(apiKey: string) {
|
|
19
|
+
const key = apiKey.trim();
|
|
20
|
+
const res = await fetch(
|
|
21
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${key}`,
|
|
22
|
+
);
|
|
23
|
+
return res.ok;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function verifyGroq(apiKey: string) {
|
|
27
|
+
const key = apiKey.trim();
|
|
28
|
+
const res = await fetch("https://api.groq.com/openai/v1/models", {
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${key}`,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return res.ok;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function verifyOpenai(apiKey: string) {
|
|
37
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `Bearer ${apiKey}`,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return res.ok;
|
|
44
|
+
}
|
|
45
|
+
async function verifyAnthropic(apiKey: string) {
|
|
46
|
+
const res = await fetch("https://api.anthropic.com/v1/models", {
|
|
47
|
+
headers: {
|
|
48
|
+
"x-api-key": apiKey,
|
|
49
|
+
"anthropic-version": "2023-06-01",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return res.ok;
|
|
54
|
+
}
|
package/config/client.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { GoogleGenAI, Type } from "@google/genai";
|
|
2
|
+
import { toolRegistery } from "../tools";
|
|
3
|
+
import { SYSTEM_PROMPT } from "./systemPrompt";
|
|
4
|
+
import type { Message, ProviderClient, StreamEvent } from "./types";
|
|
5
|
+
|
|
6
|
+
export const ACTIVE_PROVIDER_MODELS: Record<string, string> = {
|
|
7
|
+
google: "Gemini 3.5 Flash Lite",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function geminiClient(apiKey: string): ProviderClient {
|
|
11
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
async *stream(
|
|
15
|
+
messages: Message[],
|
|
16
|
+
repoContext: string,
|
|
17
|
+
signal?: AbortSignal,
|
|
18
|
+
): AsyncGenerator<StreamEvent> {
|
|
19
|
+
const contents = messages.map((message) => {
|
|
20
|
+
switch (message.role) {
|
|
21
|
+
case "user":
|
|
22
|
+
return {
|
|
23
|
+
role: "user",
|
|
24
|
+
parts: [{ text: message.content }],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
case "assistant":
|
|
28
|
+
return {
|
|
29
|
+
role: "model",
|
|
30
|
+
parts: [{ text: message.content }],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
case "assistant_tool_call":
|
|
34
|
+
return {
|
|
35
|
+
role: "model",
|
|
36
|
+
parts: [
|
|
37
|
+
{
|
|
38
|
+
functionCall: {
|
|
39
|
+
id: message.toolCallId,
|
|
40
|
+
name: message.toolName,
|
|
41
|
+
args: message.arguments,
|
|
42
|
+
},
|
|
43
|
+
thoughtSignature: message.thoughtSignature,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
case "tool":
|
|
49
|
+
return {
|
|
50
|
+
role: "user",
|
|
51
|
+
parts: [
|
|
52
|
+
{
|
|
53
|
+
functionResponse: {
|
|
54
|
+
name: message.toolName,
|
|
55
|
+
response: {
|
|
56
|
+
result: message.content,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const tools = [
|
|
65
|
+
{
|
|
66
|
+
functionDeclarations: toolRegistery.map((tool) => ({
|
|
67
|
+
name: tool.name,
|
|
68
|
+
description: tool.description,
|
|
69
|
+
parameters: {
|
|
70
|
+
type: Type.OBJECT,
|
|
71
|
+
properties: Object.fromEntries(
|
|
72
|
+
tool.parameters.map((param) => [
|
|
73
|
+
param.name,
|
|
74
|
+
{
|
|
75
|
+
type: Type.STRING,
|
|
76
|
+
description: param.description,
|
|
77
|
+
},
|
|
78
|
+
]),
|
|
79
|
+
),
|
|
80
|
+
required: tool.parameters
|
|
81
|
+
.filter((param) => param.required)
|
|
82
|
+
.map((param) => param.name),
|
|
83
|
+
},
|
|
84
|
+
})),
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
// console.time("generateContentStream");
|
|
88
|
+
// console.log("Repo Context:", repoContext.length);
|
|
89
|
+
// console.log("Messages:", JSON.stringify(contents).length);
|
|
90
|
+
// console.log("System:", SYSTEM_PROMPT.length);
|
|
91
|
+
const stream = await ai.models.generateContentStream({
|
|
92
|
+
model: "gemini-3.5-flash-lite",
|
|
93
|
+
contents,
|
|
94
|
+
|
|
95
|
+
config: {
|
|
96
|
+
systemInstruction: `${SYSTEM_PROMPT}\n\nRepository Context:\n${repoContext}`,
|
|
97
|
+
tools,
|
|
98
|
+
abortSignal: signal,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
// console.timeEnd("generateContentStream");
|
|
102
|
+
|
|
103
|
+
for await (const chunk of stream) {
|
|
104
|
+
// console.time("first-sdk-chunk");
|
|
105
|
+
// console.timeEnd("first-sdk-chunk");
|
|
106
|
+
const part = chunk.candidates?.[0]?.content?.parts?.find(
|
|
107
|
+
(p) => p.functionCall,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (part?.functionCall) {
|
|
111
|
+
yield {
|
|
112
|
+
type: "tool_call",
|
|
113
|
+
id: part.functionCall.id ?? crypto.randomUUID(),
|
|
114
|
+
name: part.functionCall.name!,
|
|
115
|
+
arguments: part.functionCall.args ?? {},
|
|
116
|
+
thoughtSignature: part.thoughtSignature,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const text = chunk.text;
|
|
122
|
+
|
|
123
|
+
if (text) {
|
|
124
|
+
yield {
|
|
125
|
+
type: "text",
|
|
126
|
+
content: text,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
yield {
|
|
132
|
+
type: "done",
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function groqClient(apiKey: string) {}
|
|
139
|
+
export function openAIClient(apiKey: string) {}
|
|
140
|
+
export function anthropicClient(apiKey: string) {}
|
|
141
|
+
|
|
142
|
+
export function createProviderClient(
|
|
143
|
+
provider: string,
|
|
144
|
+
apiKey: string,
|
|
145
|
+
): ProviderClient {
|
|
146
|
+
switch (provider) {
|
|
147
|
+
case "google":
|
|
148
|
+
|
|
149
|
+
case "gemini":
|
|
150
|
+
return geminiClient(apiKey);
|
|
151
|
+
// case "groq":
|
|
152
|
+
// return groqClient(apiKey);
|
|
153
|
+
|
|
154
|
+
// case "openai":
|
|
155
|
+
// return openAIClient(apiKey);
|
|
156
|
+
|
|
157
|
+
// case "anthropic":
|
|
158
|
+
// return anthropicClient(apiKey);
|
|
159
|
+
|
|
160
|
+
default:
|
|
161
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
162
|
+
}
|
|
163
|
+
}
|