camo-cli 2.0.1
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 +184 -0
- package/dist/agent.js +977 -0
- package/dist/art.js +33 -0
- package/dist/components/App.js +71 -0
- package/dist/components/Chat.js +509 -0
- package/dist/components/HITLConfirmation.js +89 -0
- package/dist/components/ModelSelector.js +100 -0
- package/dist/components/SetupScreen.js +43 -0
- package/dist/config/constants.js +58 -0
- package/dist/config/prompts.js +98 -0
- package/dist/config/store.js +5 -0
- package/dist/core/AgentLoop.js +159 -0
- package/dist/hooks/useAutocomplete.js +52 -0
- package/dist/hooks/useKeyboard.js +73 -0
- package/dist/index.js +31 -0
- package/dist/mcp.js +95 -0
- package/dist/memory/MemoryManager.js +228 -0
- package/dist/providers/index.js +85 -0
- package/dist/providers/registry.js +121 -0
- package/dist/providers/types.js +5 -0
- package/dist/theme.js +45 -0
- package/dist/tools/FileTools.js +88 -0
- package/dist/tools/MemoryTools.js +53 -0
- package/dist/tools/SearchTools.js +45 -0
- package/dist/tools/ShellTools.js +40 -0
- package/dist/tools/TaskTools.js +52 -0
- package/dist/tools/ToolDefinitions.js +102 -0
- package/dist/tools/ToolRegistry.js +30 -0
- package/dist/types/Agent.js +6 -0
- package/dist/types/ink.js +1 -0
- package/dist/types/message.js +1 -0
- package/dist/types/ui.js +1 -0
- package/dist/utils/CriticAgent.js +88 -0
- package/dist/utils/DecisionLogger.js +156 -0
- package/dist/utils/MessageHistory.js +55 -0
- package/dist/utils/PermissionManager.js +253 -0
- package/dist/utils/SessionManager.js +180 -0
- package/dist/utils/TaskState.js +108 -0
- package/dist/utils/debug.js +35 -0
- package/dist/utils/execAsync.js +3 -0
- package/dist/utils/retry.js +50 -0
- package/dist/utils/tokenCounter.js +24 -0
- package/dist/utils/uiFormatter.js +106 -0
- package/package.json +92 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
export const useKeyboard = () => {
|
|
3
|
+
const [input, setInput] = useState('');
|
|
4
|
+
const [cursorPos, setCursorPos] = useState(0);
|
|
5
|
+
const [history, setHistory] = useState([]);
|
|
6
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
7
|
+
const handleKey = useCallback((str, key) => {
|
|
8
|
+
// Submit
|
|
9
|
+
if (key.return) {
|
|
10
|
+
if (input.trim()) {
|
|
11
|
+
setHistory(prev => [...prev, input]);
|
|
12
|
+
// Process command here
|
|
13
|
+
setInput('');
|
|
14
|
+
setCursorPos(0);
|
|
15
|
+
setHistoryIndex(-1);
|
|
16
|
+
}
|
|
17
|
+
return { shouldSubmit: true, value: input };
|
|
18
|
+
}
|
|
19
|
+
// History navigation
|
|
20
|
+
if (key.upArrow && history.length > 0) {
|
|
21
|
+
const newIndex = Math.min(historyIndex + 1, history.length - 1);
|
|
22
|
+
setHistoryIndex(newIndex);
|
|
23
|
+
const historyValue = history[history.length - 1 - newIndex];
|
|
24
|
+
setInput(historyValue);
|
|
25
|
+
setCursorPos(historyValue.length);
|
|
26
|
+
return { shouldSubmit: false };
|
|
27
|
+
}
|
|
28
|
+
if (key.downArrow && historyIndex >= 0) {
|
|
29
|
+
const newIndex = historyIndex - 1;
|
|
30
|
+
if (newIndex < 0) {
|
|
31
|
+
setHistoryIndex(-1);
|
|
32
|
+
setInput('');
|
|
33
|
+
setCursorPos(0);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
setHistoryIndex(newIndex);
|
|
37
|
+
const historyValue = history[history.length - 1 - newIndex];
|
|
38
|
+
setInput(historyValue);
|
|
39
|
+
setCursorPos(historyValue.length);
|
|
40
|
+
}
|
|
41
|
+
return { shouldSubmit: false };
|
|
42
|
+
}
|
|
43
|
+
// Cursor movement
|
|
44
|
+
if (key.leftArrow) {
|
|
45
|
+
setCursorPos(Math.max(0, cursorPos - 1));
|
|
46
|
+
return { shouldSubmit: false };
|
|
47
|
+
}
|
|
48
|
+
if (key.rightArrow) {
|
|
49
|
+
setCursorPos(Math.min(input.length, cursorPos + 1));
|
|
50
|
+
return { shouldSubmit: false };
|
|
51
|
+
}
|
|
52
|
+
// Backspace
|
|
53
|
+
if (key.backspace && cursorPos > 0) {
|
|
54
|
+
const newInput = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
|
|
55
|
+
setInput(newInput);
|
|
56
|
+
setCursorPos(cursorPos - 1);
|
|
57
|
+
return { shouldSubmit: false };
|
|
58
|
+
}
|
|
59
|
+
// Tab for autocomplete
|
|
60
|
+
if (key.tab) {
|
|
61
|
+
return { shouldSubmit: false, shouldAutocomplete: true };
|
|
62
|
+
}
|
|
63
|
+
// Regular input
|
|
64
|
+
if (str && !key.ctrl && !key.meta) {
|
|
65
|
+
const newInput = input.slice(0, cursorPos) + str + input.slice(cursorPos);
|
|
66
|
+
setInput(newInput);
|
|
67
|
+
setCursorPos(cursorPos + str.length);
|
|
68
|
+
return { shouldSubmit: false };
|
|
69
|
+
}
|
|
70
|
+
return { shouldSubmit: false };
|
|
71
|
+
}, [input, cursorPos, history, historyIndex]);
|
|
72
|
+
return { input, cursorPos, handleKey, setInput, setCursorPos };
|
|
73
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { render } from 'ink';
|
|
5
|
+
import { App } from './components/App.js';
|
|
6
|
+
// Setup CLI program
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name('camo')
|
|
10
|
+
.description('🦎 CAMO - Elite Autonomous CLI Agent')
|
|
11
|
+
.version('2.0.0')
|
|
12
|
+
.option('-v, --verbose', 'Enable verbose output')
|
|
13
|
+
.argument('[initialInput]', 'Initial prompt to start with')
|
|
14
|
+
.action(async (initialInput, options) => {
|
|
15
|
+
// If we have an initial input, we can pass it to the App.
|
|
16
|
+
// Ideally, if it's a one-shot command, we might run headless, but user said "Start-Flow... Man landet direkt im Chat-Interface".
|
|
17
|
+
// So even with args, we might open the UI and fill the input?
|
|
18
|
+
// User didn't specify headless behavior in refactor. I'll focus on interactive UI.
|
|
19
|
+
// Start Ink with custom exit handling
|
|
20
|
+
const app = React.createElement(App, {
|
|
21
|
+
initialInput: initialInput,
|
|
22
|
+
verbose: options.verbose
|
|
23
|
+
});
|
|
24
|
+
// Render with exitOnCtrlC disabled (we handle it ourselves)
|
|
25
|
+
const { unmount, waitUntilExit } = render(app, {
|
|
26
|
+
exitOnCtrlC: false
|
|
27
|
+
});
|
|
28
|
+
// Wait for app to exit gracefully
|
|
29
|
+
await waitUntilExit();
|
|
30
|
+
});
|
|
31
|
+
program.parse();
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import * as clack from '@clack/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
export class McpManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.clients = new Map();
|
|
9
|
+
this.activeTools = new Map();
|
|
10
|
+
}
|
|
11
|
+
async connect(configs) {
|
|
12
|
+
for (const config of configs) {
|
|
13
|
+
try {
|
|
14
|
+
const transport = new StdioClientTransport({
|
|
15
|
+
command: config.command,
|
|
16
|
+
args: config.args,
|
|
17
|
+
});
|
|
18
|
+
const client = new Client({
|
|
19
|
+
name: "camo-client",
|
|
20
|
+
version: "1.0.0",
|
|
21
|
+
}, {
|
|
22
|
+
capabilities: {},
|
|
23
|
+
});
|
|
24
|
+
await client.connect(transport);
|
|
25
|
+
this.clients.set(config.name, client);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(chalk.red(`Failed to connect to MCP server '${config.name}': ${error.message}`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
getActiveServerCount() {
|
|
33
|
+
return this.clients.size;
|
|
34
|
+
}
|
|
35
|
+
async getTools() {
|
|
36
|
+
const aiSdkTools = {};
|
|
37
|
+
this.activeTools.clear();
|
|
38
|
+
for (const [serverName, client] of this.clients.entries()) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await client.listTools();
|
|
41
|
+
for (const tool of result.tools) {
|
|
42
|
+
// Prefix tool name to avoid collisions
|
|
43
|
+
const uniqueToolName = `${serverName}_${tool.name}`;
|
|
44
|
+
this.activeTools.set(uniqueToolName, { serverName, toolName: tool.name });
|
|
45
|
+
// We wrap the execution to include a security check
|
|
46
|
+
aiSdkTools[uniqueToolName] = {
|
|
47
|
+
description: `${tool.description || ''} (Provided by MCP Server: ${serverName}) \nArguments Schema: ${JSON.stringify(tool.inputSchema)}`,
|
|
48
|
+
// We use z.any() here but the description contains the schema for the LLM to follow.
|
|
49
|
+
// This prevents complex runtime Zod conversion issues.
|
|
50
|
+
inputSchema: z.object({}).passthrough().describe("JSON arguments matching the schema provided in description"),
|
|
51
|
+
execute: async (args) => {
|
|
52
|
+
return await this.executeToolWithSecurity(uniqueToolName, args);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error(chalk.yellow(`Failed to fetch tools from '${serverName}': ${error.message}`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return aiSdkTools;
|
|
62
|
+
}
|
|
63
|
+
async executeToolWithSecurity(uniqueToolName, args) {
|
|
64
|
+
const info = this.activeTools.get(uniqueToolName);
|
|
65
|
+
if (!info) {
|
|
66
|
+
return { error: `Tool ${uniqueToolName} not found` };
|
|
67
|
+
}
|
|
68
|
+
const { serverName, toolName } = info;
|
|
69
|
+
console.log(chalk.yellow(`\n[MCP SECURITY] Server '${serverName}' wants to execute tool '${toolName}'`));
|
|
70
|
+
console.log(chalk.white(`Arguments: ${JSON.stringify(args, null, 2)}`));
|
|
71
|
+
const approved = await clack.confirm({
|
|
72
|
+
message: 'Allow execution?',
|
|
73
|
+
});
|
|
74
|
+
if (!approved || clack.isCancel(approved)) {
|
|
75
|
+
return { error: 'User denied execution' };
|
|
76
|
+
}
|
|
77
|
+
const client = this.clients.get(serverName);
|
|
78
|
+
if (!client)
|
|
79
|
+
return { error: 'Client disconnected' };
|
|
80
|
+
try {
|
|
81
|
+
const result = await client.callTool({
|
|
82
|
+
name: toolName,
|
|
83
|
+
arguments: args,
|
|
84
|
+
});
|
|
85
|
+
// Convert MCP result to simple text/json for AI SDK
|
|
86
|
+
const toolResult = result;
|
|
87
|
+
return {
|
|
88
|
+
result: toolResult.content.map((c) => c.type === 'text' ? c.text : '[Binary/Image]').join('\n')
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return { error: `MCP Execution Error: ${error.message}` };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* MemoryManager - Handles persistent working memory in .camo directory
|
|
5
|
+
*/
|
|
6
|
+
export class MemoryManager {
|
|
7
|
+
constructor(projectRoot) {
|
|
8
|
+
this.camoDir = path.join(projectRoot, '.camo');
|
|
9
|
+
this.planPath = path.join(this.camoDir, 'plan.md');
|
|
10
|
+
this.contextPath = path.join(this.camoDir, 'context.md');
|
|
11
|
+
this.sessionPath = path.join(this.camoDir, 'session.json');
|
|
12
|
+
}
|
|
13
|
+
static getInstance(projectRoot = process.cwd()) {
|
|
14
|
+
if (!MemoryManager.instance) {
|
|
15
|
+
MemoryManager.instance = new MemoryManager(projectRoot);
|
|
16
|
+
}
|
|
17
|
+
return MemoryManager.instance;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Initialize .camo directory and files if they don't exist
|
|
21
|
+
*/
|
|
22
|
+
async initialize() {
|
|
23
|
+
try {
|
|
24
|
+
// Create .camo directory
|
|
25
|
+
await fs.mkdir(this.camoDir, { recursive: true });
|
|
26
|
+
// Initialize plan.md if not exists
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(this.planPath);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
await fs.writeFile(this.planPath, this.getDefaultPlan(), 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
// Initialize context.md if not exists
|
|
34
|
+
try {
|
|
35
|
+
await fs.access(this.contextPath);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
await fs.writeFile(this.contextPath, this.getDefaultContext(), 'utf-8');
|
|
39
|
+
}
|
|
40
|
+
// Initialize session.json if not exists
|
|
41
|
+
try {
|
|
42
|
+
await fs.access(this.sessionPath);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
const defaultSession = {
|
|
46
|
+
totalTokens: 0,
|
|
47
|
+
lastCheckpoint: new Date().toISOString(),
|
|
48
|
+
sessionCount: 0
|
|
49
|
+
};
|
|
50
|
+
await fs.writeFile(this.sessionPath, JSON.stringify(defaultSession, null, 2), 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
// Add .camo to .gitignore if .gitignore exists
|
|
53
|
+
await this.addToGitignore();
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
console.error('Failed to initialize .camo directory:', e.message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Add .camo/ to .gitignore if not already present
|
|
61
|
+
*/
|
|
62
|
+
async addToGitignore() {
|
|
63
|
+
const gitignorePath = path.join(path.dirname(this.camoDir), '.gitignore');
|
|
64
|
+
try {
|
|
65
|
+
let gitignoreContent = '';
|
|
66
|
+
try {
|
|
67
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// .gitignore doesn't exist, create it
|
|
71
|
+
}
|
|
72
|
+
if (!gitignoreContent.includes('.camo')) {
|
|
73
|
+
const newContent = gitignoreContent + (gitignoreContent.endsWith('\n') ? '' : '\n') + '.camo/\n';
|
|
74
|
+
await fs.writeFile(gitignorePath, newContent, 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
// Ignore errors - .gitignore is optional
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Load plan.md content
|
|
83
|
+
*/
|
|
84
|
+
async loadPlan() {
|
|
85
|
+
try {
|
|
86
|
+
return await fs.readFile(this.planPath, 'utf-8');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Save plan.md content
|
|
94
|
+
*/
|
|
95
|
+
async savePlan(content) {
|
|
96
|
+
await fs.writeFile(this.planPath, content, 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Update task status in plan.md
|
|
100
|
+
*/
|
|
101
|
+
async updateTaskStatus(taskId, status) {
|
|
102
|
+
const plan = await this.loadPlan();
|
|
103
|
+
const statusChar = status === 'complete' ? 'x' : status === 'in_progress' ? '/' : ' ';
|
|
104
|
+
// Replace task status (simple regex-based approach)
|
|
105
|
+
const updatedPlan = plan.replace(new RegExp(`- \\[.\\] (.*)<!-- id: ${taskId} -->`, 'g'), `- [${statusChar}] $1<!-- id: ${taskId} -->`);
|
|
106
|
+
await this.savePlan(updatedPlan);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Load context.md content
|
|
110
|
+
*/
|
|
111
|
+
async loadContext() {
|
|
112
|
+
try {
|
|
113
|
+
return await fs.readFile(this.contextPath, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Add or update a fact in context.md
|
|
121
|
+
*/
|
|
122
|
+
async updateContext(category, content) {
|
|
123
|
+
let context = await this.loadContext();
|
|
124
|
+
// Check if category already exists
|
|
125
|
+
const categoryHeader = `## ${category}`;
|
|
126
|
+
if (context.includes(categoryHeader)) {
|
|
127
|
+
// Append to existing category
|
|
128
|
+
const lines = context.split('\n');
|
|
129
|
+
const categoryIndex = lines.findIndex(l => l.startsWith(categoryHeader));
|
|
130
|
+
// Find next category or end
|
|
131
|
+
let nextCategoryIndex = lines.length;
|
|
132
|
+
for (let i = categoryIndex + 1; i < lines.length; i++) {
|
|
133
|
+
if (lines[i].startsWith('## ')) {
|
|
134
|
+
nextCategoryIndex = i;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Insert before next category
|
|
139
|
+
lines.splice(nextCategoryIndex, 0, `- ${content}`);
|
|
140
|
+
context = lines.join('\n');
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Add new category
|
|
144
|
+
context += `\n${categoryHeader}\n- ${content}\n`;
|
|
145
|
+
}
|
|
146
|
+
await fs.writeFile(this.contextPath, context, 'utf-8');
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Load session.json data
|
|
150
|
+
*/
|
|
151
|
+
async loadSession() {
|
|
152
|
+
try {
|
|
153
|
+
const content = await fs.readFile(this.sessionPath, 'utf-8');
|
|
154
|
+
return JSON.parse(content);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return {
|
|
158
|
+
totalTokens: 0,
|
|
159
|
+
lastCheckpoint: new Date().toISOString(),
|
|
160
|
+
sessionCount: 0
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Save session.json data
|
|
166
|
+
*/
|
|
167
|
+
async saveSession(data) {
|
|
168
|
+
const current = await this.loadSession();
|
|
169
|
+
const updated = { ...current, ...data };
|
|
170
|
+
await fs.writeFile(this.sessionPath, JSON.stringify(updated, null, 2), 'utf-8');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Increment session count
|
|
174
|
+
*/
|
|
175
|
+
async incrementSession() {
|
|
176
|
+
const session = await this.loadSession();
|
|
177
|
+
await this.saveSession({
|
|
178
|
+
sessionCount: session.sessionCount + 1,
|
|
179
|
+
lastCheckpoint: new Date().toISOString()
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get pending tasks from plan.md
|
|
184
|
+
*/
|
|
185
|
+
async getPendingTasks() {
|
|
186
|
+
const plan = await this.loadPlan();
|
|
187
|
+
const lines = plan.split('\n');
|
|
188
|
+
const pending = [];
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
if (line.match(/- \[ \]/)) {
|
|
191
|
+
// Extract task text (remove checkbox)
|
|
192
|
+
const task = line.replace(/- \[ \]/, '').trim();
|
|
193
|
+
pending.push(task);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return pending;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Default plan.md template
|
|
200
|
+
*/
|
|
201
|
+
getDefaultPlan() {
|
|
202
|
+
return `# Current Plan
|
|
203
|
+
|
|
204
|
+
## Active Tasks
|
|
205
|
+
<!-- Add tasks here using: - [ ] Task description -->
|
|
206
|
+
|
|
207
|
+
## Completed
|
|
208
|
+
<!-- Completed tasks will be moved here -->
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Default context.md template
|
|
213
|
+
*/
|
|
214
|
+
getDefaultContext() {
|
|
215
|
+
return `# Project Context
|
|
216
|
+
|
|
217
|
+
## Architecture
|
|
218
|
+
<!-- Learned architectural decisions go here -->
|
|
219
|
+
|
|
220
|
+
## Key Files
|
|
221
|
+
<!-- Important file locations -->
|
|
222
|
+
|
|
223
|
+
## Decisions
|
|
224
|
+
<!-- Technical decisions and rationale -->
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export default MemoryManager;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Manager
|
|
3
|
+
* Central management for AI providers
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_PROVIDERS } from './registry.js';
|
|
6
|
+
export class ProviderManager {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
}
|
|
10
|
+
getProviders() {
|
|
11
|
+
const providers = this.config.get('providers', {});
|
|
12
|
+
const merged = {};
|
|
13
|
+
// Merge defaults
|
|
14
|
+
Object.entries(DEFAULT_PROVIDERS).forEach(([key, defaultProvider]) => {
|
|
15
|
+
// Get API key from root config if it existed there (legacy) or provider config
|
|
16
|
+
// We'll primarily look in the provider config object now, or mapped keys in App.tsx logic used config.get('googleApiKey').
|
|
17
|
+
// To align with App.tsx which uses `googleApiKey` etc at root:
|
|
18
|
+
let apiKey = '';
|
|
19
|
+
if (key === 'google')
|
|
20
|
+
apiKey = this.config.get('googleApiKey');
|
|
21
|
+
if (key === 'anthropic')
|
|
22
|
+
apiKey = this.config.get('anthropicApiKey');
|
|
23
|
+
if (key === 'openai')
|
|
24
|
+
apiKey = this.config.get('openaiApiKey');
|
|
25
|
+
if (key === 'openrouter')
|
|
26
|
+
apiKey = this.config.get('openrouterApiKey');
|
|
27
|
+
if (key === 'custom')
|
|
28
|
+
apiKey = this.config.get('customApiKey');
|
|
29
|
+
merged[key] = {
|
|
30
|
+
...defaultProvider,
|
|
31
|
+
apiKey: apiKey || '',
|
|
32
|
+
isActive: !!apiKey
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
// Custom Base URL handling
|
|
36
|
+
if (merged['custom']) {
|
|
37
|
+
merged['custom'].baseURL = this.config.get('customBaseUrl') || '';
|
|
38
|
+
}
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
|
41
|
+
getActiveSelection() {
|
|
42
|
+
const activeProvider = this.config.get('activeProvider'); // defaults?
|
|
43
|
+
const activeModel = this.config.get('activeModel');
|
|
44
|
+
// If explicitly set and valid, use it
|
|
45
|
+
if (activeProvider && activeModel) {
|
|
46
|
+
const providers = this.getProviders();
|
|
47
|
+
if (providers[activeProvider]) {
|
|
48
|
+
const model = providers[activeProvider].models.find(m => m.id === activeModel);
|
|
49
|
+
if (model)
|
|
50
|
+
return { provider: activeProvider, model };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return this.getDefaultSelection();
|
|
54
|
+
}
|
|
55
|
+
getDefaultSelection() {
|
|
56
|
+
const providers = this.getProviders();
|
|
57
|
+
// Priority: Google first (as requested)
|
|
58
|
+
const priorityOrder = ['google', 'anthropic', 'openai', 'openrouter', 'custom'];
|
|
59
|
+
for (const key of priorityOrder) {
|
|
60
|
+
const p = providers[key];
|
|
61
|
+
if (p?.isActive && p.models.length > 0) {
|
|
62
|
+
return { provider: key, model: p.models[0] };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null; // No active provider with key
|
|
66
|
+
}
|
|
67
|
+
async createModel(selection) {
|
|
68
|
+
const { createGoogleGenerativeAI } = await import('@ai-sdk/google');
|
|
69
|
+
const activeSelection = selection || this.getActiveSelection();
|
|
70
|
+
if (!activeSelection)
|
|
71
|
+
throw new Error('No active provider configured.');
|
|
72
|
+
const providers = this.getProviders();
|
|
73
|
+
const provider = providers[activeSelection.provider];
|
|
74
|
+
if (!provider.apiKey)
|
|
75
|
+
throw new Error(`Missing API Key for ${provider.name}`);
|
|
76
|
+
const modelId = activeSelection.model.id;
|
|
77
|
+
// Only Google is supported
|
|
78
|
+
if (provider.type === 'google') {
|
|
79
|
+
return createGoogleGenerativeAI({ apiKey: provider.apiKey })(modelId);
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`Only Google provider is supported. Got: ${provider.type}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export * from './types.js';
|
|
85
|
+
export * from './registry.js';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Registry
|
|
3
|
+
* Pre-configured providers and their available models
|
|
4
|
+
*/
|
|
5
|
+
// Anthropic Models
|
|
6
|
+
const anthropicModels = [
|
|
7
|
+
{
|
|
8
|
+
id: 'claude-3-5-sonnet-latest',
|
|
9
|
+
name: 'Claude 3.5 Sonnet',
|
|
10
|
+
contextWindow: 200000,
|
|
11
|
+
supportsTools: true,
|
|
12
|
+
description: 'Most capable model for coding tasks'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'claude-3-5-haiku-latest',
|
|
16
|
+
name: 'Claude 3.5 Haiku',
|
|
17
|
+
contextWindow: 200000,
|
|
18
|
+
supportsTools: true,
|
|
19
|
+
description: 'Fast and efficient for quick tasks'
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
// OpenAI Models
|
|
23
|
+
const openaiModels = [
|
|
24
|
+
{
|
|
25
|
+
id: 'gpt-4o',
|
|
26
|
+
name: 'GPT-4o',
|
|
27
|
+
contextWindow: 128000,
|
|
28
|
+
supportsTools: true,
|
|
29
|
+
description: 'Most capable GPT-4 model'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'gpt-4o-mini',
|
|
33
|
+
name: 'GPT-4o Mini',
|
|
34
|
+
contextWindow: 128000,
|
|
35
|
+
supportsTools: true,
|
|
36
|
+
description: 'Faster and more affordable'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'o1',
|
|
40
|
+
name: 'O1',
|
|
41
|
+
contextWindow: 200000,
|
|
42
|
+
supportsTools: false,
|
|
43
|
+
description: 'Advanced reasoning model'
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
// Google Models
|
|
47
|
+
const googleModels = [
|
|
48
|
+
{
|
|
49
|
+
id: 'gemini-2.0-flash',
|
|
50
|
+
name: 'Gemini 2.0 Flash',
|
|
51
|
+
contextWindow: 1000000,
|
|
52
|
+
supportsTools: true,
|
|
53
|
+
description: 'Fast with massive context window (Default)'
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
// OpenRouter Models (Example)
|
|
57
|
+
const openRouterModels = [
|
|
58
|
+
{
|
|
59
|
+
id: 'google/gemini-2.0-flash-exp:free',
|
|
60
|
+
name: 'Gemini 2.0 Flash (Free)',
|
|
61
|
+
contextWindow: 1000000,
|
|
62
|
+
supportsTools: true,
|
|
63
|
+
description: 'Free tier via OpenRouter'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'anthropic/claude-3.5-sonnet',
|
|
67
|
+
name: 'Claude 3.5 Sonnet (OR)',
|
|
68
|
+
contextWindow: 200000,
|
|
69
|
+
supportsTools: true,
|
|
70
|
+
description: 'Via OpenRouter'
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
// Custom/Self-hosted Models
|
|
74
|
+
const customModels = [
|
|
75
|
+
{
|
|
76
|
+
id: 'custom',
|
|
77
|
+
name: 'Custom Model',
|
|
78
|
+
contextWindow: 4000,
|
|
79
|
+
supportsTools: true,
|
|
80
|
+
description: 'Custom API endpoint'
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* Default provider configurations
|
|
85
|
+
*/
|
|
86
|
+
export const DEFAULT_PROVIDERS = {
|
|
87
|
+
google: {
|
|
88
|
+
name: 'Google',
|
|
89
|
+
type: 'google',
|
|
90
|
+
models: googleModels
|
|
91
|
+
},
|
|
92
|
+
anthropic: {
|
|
93
|
+
name: 'Anthropic',
|
|
94
|
+
type: 'anthropic',
|
|
95
|
+
models: anthropicModels
|
|
96
|
+
},
|
|
97
|
+
openai: {
|
|
98
|
+
name: 'OpenAI',
|
|
99
|
+
type: 'openai',
|
|
100
|
+
models: openaiModels
|
|
101
|
+
},
|
|
102
|
+
openrouter: {
|
|
103
|
+
name: 'OpenRouter',
|
|
104
|
+
type: 'openrouter',
|
|
105
|
+
models: openRouterModels
|
|
106
|
+
},
|
|
107
|
+
custom: {
|
|
108
|
+
name: 'Custom',
|
|
109
|
+
type: 'custom',
|
|
110
|
+
models: customModels
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
export function findModel(modelId) {
|
|
114
|
+
for (const [providerKey, provider] of Object.entries(DEFAULT_PROVIDERS)) {
|
|
115
|
+
const model = provider.models.find(m => m.id === modelId);
|
|
116
|
+
if (model) {
|
|
117
|
+
return { provider: providerKey, model };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimalist Design System
|
|
3
|
+
* Black/White/Gray color palette with high contrast
|
|
4
|
+
*/
|
|
5
|
+
export const theme = {
|
|
6
|
+
colors: {
|
|
7
|
+
white: '#FFFFFF',
|
|
8
|
+
lightGray: '#D3D3D3',
|
|
9
|
+
mediumGray: '#808080',
|
|
10
|
+
darkGray: '#333333',
|
|
11
|
+
black: '#000000',
|
|
12
|
+
accent: '#FFFFFF', // For selection/active elements
|
|
13
|
+
success: '#00FF00',
|
|
14
|
+
error: '#FF0000',
|
|
15
|
+
warning: '#FFFF00',
|
|
16
|
+
},
|
|
17
|
+
textColors: {
|
|
18
|
+
primary: 'white',
|
|
19
|
+
secondary: 'lightGray',
|
|
20
|
+
muted: 'mediumGray',
|
|
21
|
+
inverse: 'black',
|
|
22
|
+
},
|
|
23
|
+
backgrounds: {
|
|
24
|
+
default: 'black',
|
|
25
|
+
selected: 'white',
|
|
26
|
+
muted: 'darkGray',
|
|
27
|
+
},
|
|
28
|
+
spacing: {
|
|
29
|
+
xs: 1,
|
|
30
|
+
sm: 2,
|
|
31
|
+
md: 4,
|
|
32
|
+
lg: 8,
|
|
33
|
+
},
|
|
34
|
+
borders: {
|
|
35
|
+
light: '─',
|
|
36
|
+
heavy: '━',
|
|
37
|
+
corner: '┬',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
// Prefix symbols for messages
|
|
41
|
+
export const messagePrefixes = {
|
|
42
|
+
user: '> ',
|
|
43
|
+
assistant: '- ',
|
|
44
|
+
system: '~ ',
|
|
45
|
+
};
|