Skip to content

Commit d84688c

Browse files
committed
feat: add ai agent support with interactive ui
1 parent 9981663 commit d84688c

File tree

20 files changed

+972
-31
lines changed

20 files changed

+972
-31
lines changed

package-lock.json

Lines changed: 301 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,23 @@
4646
},
4747
"license": "MIT",
4848
"dependencies": {
49+
"@ai-sdk/anthropic": "^2.0.9",
50+
"@ai-sdk/google": "^2.0.11",
51+
"@ai-sdk/groq": "^2.0.16",
52+
"@ai-sdk/openai": "^2.0.23",
53+
"@anthropic-ai/sdk": "^0.60.0",
4954
"@inkjs/ui": "^2.0.0",
5055
"@modelcontextprotocol/sdk": "^1.17.4",
56+
"ai": "^5.0.28",
57+
"boxen": "^8.0.1",
5158
"chalk": "^5.6.0",
5259
"commander": "^14.0.0",
60+
"dedent": "^1.6.0",
5361
"dotenv": "^17.2.1",
5462
"ink": "^6.2.2",
5563
"ink-gradient": "^3.0.0",
64+
"ink-select-input": "^6.2.0",
65+
"ink-text-input": "^6.0.0",
5666
"object-hash": "^3.0.0",
5767
"react": "^19.1.1",
5868
"winston": "^3.17.0"

src/agents/AIAgent.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { generateText, type CoreMessage } from "ai";
2+
import type { ToolDefinition } from "../tools/types.js";
3+
import type { Dispatch, SetStateAction } from "react";
4+
5+
import { providerEnvVar } from "./constants.js";
6+
import type { Message, Provider } from "./types.js";
7+
import { ProviderFactory } from "./providers.js";
8+
9+
// Enum-like MODEL_TYPES for factory selection
10+
export const MODEL_TYPES = {
11+
OPENAI: "openai",
12+
GOOGLE: "google",
13+
ANTHROPIC: "anthropic",
14+
GROQ: "groq",
15+
} as const;
16+
17+
export class AIAgent {
18+
private provider: Provider;
19+
private model: string;
20+
private conversation: CoreMessage[] = [];
21+
private apiKey: string | undefined;
22+
23+
constructor(
24+
provider: Provider,
25+
model: string,
26+
private tools: ToolDefinition[],
27+
private setIsProcessing: Dispatch<SetStateAction<boolean>>,
28+
private setMessages: Dispatch<SetStateAction<Message[]>>,
29+
) {
30+
this.provider = provider;
31+
this.model = model;
32+
33+
const envVarName = providerEnvVar[this.provider as string];
34+
this.apiKey = envVarName ? process.env[envVarName] : undefined;
35+
}
36+
37+
async processMessage(userInput: string): Promise<void> {
38+
this.setIsProcessing(true);
39+
40+
// Track conversation for context
41+
this.conversation.push({ role: "user", content: userInput });
42+
43+
try {
44+
const model = this.createModel();
45+
if (!model) {
46+
throw new Error(
47+
`Missing API key for provider: ${this.provider}. Set ${providerEnvVar[this.provider]}`,
48+
);
49+
}
50+
51+
const { text } = await generateText({
52+
model,
53+
messages: this.conversation,
54+
});
55+
56+
// Append assistant message to conversation and UI
57+
this.conversation.push({ role: "assistant", content: text });
58+
this.setMessages((prev) => [
59+
...prev,
60+
{ role: "assistant", content: text, timestamp: new Date() },
61+
]);
62+
} catch (error) {
63+
const msg = error instanceof Error ? error.message : String(error);
64+
this.setMessages((prev) => [
65+
...prev,
66+
{ role: "system", content: `❌ Error: ${msg}`, timestamp: new Date() },
67+
]);
68+
} finally {
69+
this.setIsProcessing(false);
70+
}
71+
}
72+
73+
private createModel() {
74+
return ProviderFactory.create(this.provider, this.model, this.apiKey);
75+
}
76+
}

src/agents/config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { getConfigManager } from "../config/manager.js";
2+
import type { Provider } from "./types.js";
3+
4+
export type AgentConfig = {
5+
provider?: Provider;
6+
model?: string;
7+
};
8+
9+
export function loadAgentConfig(): AgentConfig {
10+
const manager = getConfigManager();
11+
const settings = manager.loadProjectSettings();
12+
13+
const provider = settings.provider || undefined;
14+
const model = settings.model || undefined;
15+
16+
return { provider, model };
17+
}

src/agents/constants.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export const providers = [
2+
{ label: "anthropic", value: "anthropic" },
3+
{ label: "google", value: "google" },
4+
{ label: "openAI", value: "openai" },
5+
{ label: "groq", value: "groq" },
6+
];
7+
8+
// Static model lists per provider (can be extended later)
9+
export const providerModels: Record<
10+
string,
11+
{ label: string; value: string }[]
12+
> = {
13+
openai: [
14+
{ label: "gpt-4o", value: "gpt-4o" },
15+
{ label: "gpt-4o-mini", value: "gpt-4o-mini" },
16+
{ label: "gpt-4.1-mini", value: "gpt-4.1-mini" },
17+
{ label: "o3-mini", value: "o3-mini" },
18+
],
19+
anthropic: [
20+
{ label: "claude-3-5-sonnet", value: "claude-3-5-sonnet-20240620" },
21+
{ label: "claude-3-opus", value: "claude-3-opus-20240229" },
22+
{ label: "claude-3-haiku", value: "claude-3-haiku-20240307" },
23+
],
24+
google: [
25+
{ label: "gemini-1.5-pro", value: "gemini-1.5-pro" },
26+
{ label: "gemini-1.5-flash", value: "gemini-1.5-flash" },
27+
],
28+
groq: [
29+
{ label: "llama-3.1-70b-versatile", value: "llama-3.1-70b-versatile" },
30+
{ label: "llama-3.1-8b-instant", value: "llama-3.1-8b-instant" },
31+
{ label: "mixtral-8x7b-32768", value: "mixtral-8x7b-32768" },
32+
],
33+
};
34+
35+
// Required API env var per provider
36+
export const providerEnvVar: Record<string, string> = {
37+
openai: "OPENAI_API_KEY",
38+
anthropic: "ANTHROPIC_API_KEY",
39+
google: "GOOGLE_API_KEY",
40+
groq: "GROQ_API_KEY",
41+
};

src/agents/providers.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { createGroq } from "@ai-sdk/groq";
2+
import { createOpenAI } from "@ai-sdk/openai";
3+
import { createAnthropic } from "@ai-sdk/anthropic";
4+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
5+
import type { Provider } from "./types.js";
6+
import { MODEL_TYPES } from "./AIAgent.js";
7+
8+
export class ProviderFactory {
9+
static create(type: Provider, modelName: string, apiKey?: string) {
10+
if (!apiKey) return undefined;
11+
12+
switch (type) {
13+
case MODEL_TYPES.OPENAI: {
14+
return createOpenAI({ apiKey })(modelName);
15+
}
16+
case MODEL_TYPES.GOOGLE: {
17+
const name = modelName?.startsWith("models/")
18+
? modelName
19+
: `models/${modelName}`;
20+
return createGoogleGenerativeAI({ apiKey })(name);
21+
}
22+
case MODEL_TYPES.ANTHROPIC: {
23+
return createAnthropic({ apiKey })(modelName);
24+
}
25+
case MODEL_TYPES.GROQ: {
26+
return createGroq({ apiKey })(modelName);
27+
}
28+
default:
29+
return undefined;
30+
}
31+
}
32+
}

src/agents/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type Provider = "openai" | "google" | "anthropic" | "groq";
2+
export type Message = {
3+
role: "user" | "assistant" | "tool" | "system";
4+
content: string;
5+
timestamp: Date;
6+
toolName?: string;
7+
};

src/commands/mcp.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ mcp
9696
console.log("Starting MCP server health check...");
9797
const { waitUntilExit } = render(React.createElement(MCPList, { verbose }));
9898

99-
await waitUntilExit();
99+
waitUntilExit().then(() => process.exit(0));
100100
});
101101

102102
mcp
@@ -121,5 +121,5 @@ mcp
121121
.description("Test connection to an MCP server")
122122
.action(async (name: string) => {
123123
const { waitUntilExit } = render(React.createElement(MCPTest, { name }));
124-
await waitUntilExit();
124+
waitUntilExit().then(() => process.exit(0));
125125
});

src/components/mcp/mcp-list.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { useEffect } from "react";
33
import { Spinner } from "@inkjs/ui";
44

55
import { useMCPServers } from "./useMCPServers.js";
6-
import { MCPEmpty, MCPError, MCPSummary, MCPTable } from "./mcp-base.js";
6+
import { MCPEmpty, MCPError, MCPSummary } from "./mcp-base.js";
77
import Table from "../ink-table.js";
88

99
export function MCPList({ verbose = false }: { verbose?: boolean }) {
1010
const { exit } = useApp();
11-
const { loading, rows, summary, error } = useMCPServers(exit, verbose);
11+
const { loading, rows, summary, error } = useMCPServers(verbose);
1212

1313
// After data is ready and rendered, politely exit to finalize output
1414
useEffect(() => {

src/components/mcp/mcp-test.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useApp } from "ink";
21
import { useMCPTest } from "./useMCPTest.js";
32
import {
43
ConnectingState,
@@ -8,8 +7,7 @@ import {
87
} from "./mcp-base.js";
98

109
export function MCPTest({ name }: { name: string }) {
11-
const { exit } = useApp();
12-
const { phase, error, transport, tools } = useMCPTest(name, exit);
10+
const { phase, error, transport, tools } = useMCPTest(name);
1311

1412
if (phase === "connecting")
1513
return <ConnectingState name={name} transport={transport} />;

0 commit comments

Comments
 (0)