activo 0.2.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 (86) hide show
  1. package/FINAL_SIMPLIFIED_SPEC.md +456 -0
  2. package/README.md +62 -0
  3. package/TODO.md +193 -0
  4. package/dist/cli/banner.d.ts +3 -0
  5. package/dist/cli/banner.d.ts.map +1 -0
  6. package/dist/cli/banner.js +30 -0
  7. package/dist/cli/banner.js.map +1 -0
  8. package/dist/cli/headless.d.ts +3 -0
  9. package/dist/cli/headless.d.ts.map +1 -0
  10. package/dist/cli/headless.js +34 -0
  11. package/dist/cli/headless.js.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +42 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/core/agent.d.ts +23 -0
  17. package/dist/core/agent.d.ts.map +1 -0
  18. package/dist/core/agent.js +171 -0
  19. package/dist/core/agent.js.map +1 -0
  20. package/dist/core/config.d.ts +24 -0
  21. package/dist/core/config.d.ts.map +1 -0
  22. package/dist/core/config.js +66 -0
  23. package/dist/core/config.js.map +1 -0
  24. package/dist/core/llm/ollama.d.ts +30 -0
  25. package/dist/core/llm/ollama.d.ts.map +1 -0
  26. package/dist/core/llm/ollama.js +173 -0
  27. package/dist/core/llm/ollama.js.map +1 -0
  28. package/dist/core/mcp/client.d.ts +22 -0
  29. package/dist/core/mcp/client.d.ts.map +1 -0
  30. package/dist/core/mcp/client.js +116 -0
  31. package/dist/core/mcp/client.js.map +1 -0
  32. package/dist/core/tools/builtIn.d.ts +9 -0
  33. package/dist/core/tools/builtIn.d.ts.map +1 -0
  34. package/dist/core/tools/builtIn.js +219 -0
  35. package/dist/core/tools/builtIn.js.map +1 -0
  36. package/dist/core/tools/index.d.ts +13 -0
  37. package/dist/core/tools/index.d.ts.map +1 -0
  38. package/dist/core/tools/index.js +43 -0
  39. package/dist/core/tools/index.js.map +1 -0
  40. package/dist/core/tools/standards.d.ts +6 -0
  41. package/dist/core/tools/standards.d.ts.map +1 -0
  42. package/dist/core/tools/standards.js +215 -0
  43. package/dist/core/tools/standards.js.map +1 -0
  44. package/dist/core/tools/types.d.ts +34 -0
  45. package/dist/core/tools/types.d.ts.map +1 -0
  46. package/dist/core/tools/types.js +2 -0
  47. package/dist/core/tools/types.js.map +1 -0
  48. package/dist/ui/App.d.ts +10 -0
  49. package/dist/ui/App.d.ts.map +1 -0
  50. package/dist/ui/App.js +167 -0
  51. package/dist/ui/App.js.map +1 -0
  52. package/dist/ui/components/InputBox.d.ts +11 -0
  53. package/dist/ui/components/InputBox.d.ts.map +1 -0
  54. package/dist/ui/components/InputBox.js +7 -0
  55. package/dist/ui/components/InputBox.js.map +1 -0
  56. package/dist/ui/components/MessageList.d.ts +17 -0
  57. package/dist/ui/components/MessageList.d.ts.map +1 -0
  58. package/dist/ui/components/MessageList.js +18 -0
  59. package/dist/ui/components/MessageList.js.map +1 -0
  60. package/dist/ui/components/StatusBar.d.ts +9 -0
  61. package/dist/ui/components/StatusBar.d.ts.map +1 -0
  62. package/dist/ui/components/StatusBar.js +6 -0
  63. package/dist/ui/components/StatusBar.js.map +1 -0
  64. package/dist/ui/components/ToolStatus.d.ts +8 -0
  65. package/dist/ui/components/ToolStatus.d.ts.map +1 -0
  66. package/dist/ui/components/ToolStatus.js +7 -0
  67. package/dist/ui/components/ToolStatus.js.map +1 -0
  68. package/package.json +64 -0
  69. package/screenshot.png +0 -0
  70. package/src/cli/banner.ts +34 -0
  71. package/src/cli/headless.ts +37 -0
  72. package/src/cli/index.ts +53 -0
  73. package/src/core/agent.ts +235 -0
  74. package/src/core/config.ts +98 -0
  75. package/src/core/llm/ollama.ts +238 -0
  76. package/src/core/mcp/client.ts +143 -0
  77. package/src/core/tools/builtIn.ts +221 -0
  78. package/src/core/tools/index.ts +53 -0
  79. package/src/core/tools/standards.ts +246 -0
  80. package/src/core/tools/types.ts +37 -0
  81. package/src/ui/App.tsx +238 -0
  82. package/src/ui/components/InputBox.tsx +37 -0
  83. package/src/ui/components/MessageList.tsx +80 -0
  84. package/src/ui/components/StatusBar.tsx +36 -0
  85. package/src/ui/components/ToolStatus.tsx +38 -0
  86. package/tsconfig.json +21 -0
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "activo",
3
+ "version": "0.2.0",
4
+ "description": "AI-powered code quality analyzer with React Ink TUI, Tool Calling, and MCP support",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/mhb8436/activo.git"
8
+ },
9
+ "homepage": "https://github.com/mhb8436/activo",
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "type": "module",
13
+ "bin": {
14
+ "activo": "dist/cli/index.js"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsx src/cli/index.ts",
19
+ "start": "node dist/cli/index.js",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest watch",
22
+ "lint": "eslint src/",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "keywords": [
26
+ "code-quality",
27
+ "cli",
28
+ "ollama",
29
+ "ai",
30
+ "mcp",
31
+ "tool-calling",
32
+ "react-ink"
33
+ ],
34
+ "author": "",
35
+ "license": "MIT",
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "dependencies": {
40
+ "@anthropic-ai/sdk": "^0.52.0",
41
+ "@modelcontextprotocol/sdk": "^1.24.0",
42
+ "chalk": "^5.4.1",
43
+ "commander": "^14.0.0",
44
+ "date-fns": "^4.1.0",
45
+ "glob": "^11.0.0",
46
+ "gradient-string": "^3.0.0",
47
+ "ink": "^6.1.0",
48
+ "ink-spinner": "^5.0.0",
49
+ "ink-text-input": "^6.0.0",
50
+ "pdf-parse": "^1.1.1",
51
+ "react": "^19.1.0",
52
+ "uuid": "^10.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/gradient-string": "^1.1.6",
56
+ "@types/node": "^22.10.0",
57
+ "@types/pdf-parse": "^1.1.4",
58
+ "@types/react": "^19.1.0",
59
+ "@types/uuid": "^10.0.0",
60
+ "tsx": "^4.20.0",
61
+ "typescript": "^5.8.0",
62
+ "vitest": "^3.2.0"
63
+ }
64
+ }
package/screenshot.png ADDED
Binary file
@@ -0,0 +1,34 @@
1
+ import gradient from "gradient-string";
2
+
3
+ const ACTIVO_ASCII = `
4
+ _ ____ _____ _____ _____
5
+ / \\ / ___|_ _|_ _\\ \\ / / _ \\
6
+ / _ \\| | | | | | \\ \\ / / | | |
7
+ / ___ \\ |___ | | | | \\ V /| |_| |
8
+ /_/ \\_\\____| |_| |___| \\_/ \\___/
9
+ `;
10
+
11
+ const SUBTITLE = "AI-Powered Code Quality Analyzer";
12
+ const VERSION = "v0.2.0";
13
+
14
+ export function showBanner(): void {
15
+ const activoGradient = gradient(["#00d4ff", "#7b2ff7", "#f107a3"]);
16
+
17
+ console.log(activoGradient.multiline(ACTIVO_ASCII));
18
+ console.log();
19
+ console.log(` ${gradient(["#7b2ff7", "#00d4ff"])(SUBTITLE)} ${VERSION}`);
20
+ console.log();
21
+ console.log(" Type your request in natural language.");
22
+ console.log(" Examples:");
23
+ console.log(" • 이 프로젝트의 코드 품질을 분석해줘");
24
+ console.log(" • PDF 파일을 마크다운 규칙으로 변환해줘");
25
+ console.log(" • src 폴더의 명명규칙 위반을 찾아줘");
26
+ console.log();
27
+ console.log(" Press Ctrl+C twice to exit");
28
+ console.log("─".repeat(50));
29
+ console.log();
30
+ }
31
+
32
+ export function getShortBanner(): string {
33
+ return gradient(["#00d4ff", "#7b2ff7"])("ACTIVO") + " - Code Quality Analyzer";
34
+ }
@@ -0,0 +1,37 @@
1
+ import { OllamaClient } from "../core/llm/ollama.js";
2
+ import { processMessage } from "../core/agent.js";
3
+ import { Config } from "../core/config.js";
4
+ import chalk from "chalk";
5
+
6
+ export async function runHeadless(prompt: string | undefined, config: Config): Promise<void> {
7
+ if (!prompt) {
8
+ console.error(chalk.red("Error: Prompt is required in headless mode"));
9
+ console.error(chalk.yellow("Usage: activo -p \"your prompt here\""));
10
+ process.exit(1);
11
+ }
12
+
13
+ const client = new OllamaClient(config.ollama);
14
+
15
+ // Check connection
16
+ const isConnected = await client.isConnected();
17
+ if (!isConnected) {
18
+ console.error(chalk.red("Error: Cannot connect to Ollama"));
19
+ console.error(chalk.yellow(`Make sure Ollama is running at ${config.ollama.baseUrl}`));
20
+ process.exit(1);
21
+ }
22
+
23
+ try {
24
+ const result = await processMessage(prompt, [], client, config, (event) => {
25
+ if (event.type === "tool_use") {
26
+ console.error(chalk.dim(`[Tool] ${event.tool}: ${event.status}`));
27
+ } else if (event.type === "thinking") {
28
+ // Skip thinking in headless mode
29
+ }
30
+ });
31
+
32
+ console.log(result.content);
33
+ } catch (error) {
34
+ console.error(chalk.red(`Error: ${error}`));
35
+ process.exit(1);
36
+ }
37
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { render } from "ink";
5
+ import React from "react";
6
+ import { App } from "../ui/App.js";
7
+ import { showBanner } from "./banner.js";
8
+ import { loadConfig } from "../core/config.js";
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name("activo")
14
+ .description("AI-powered code quality analyzer with Tool Calling and MCP support")
15
+ .version("0.2.0", "-v, --version", "Display version number");
16
+
17
+ program
18
+ .option("-p, --print", "Non-interactive mode (print and exit)")
19
+ .option("--headless", "Headless mode for CI/CD")
20
+ .option("--resume", "Resume from last session")
21
+ .option("--model <model>", "Specify Ollama model")
22
+ .argument("[prompt]", "Initial prompt")
23
+ .action(async (prompt, options) => {
24
+ // Show ASCII banner
25
+ showBanner();
26
+
27
+ // Load config
28
+ const config = loadConfig();
29
+
30
+ if (options.model) {
31
+ config.ollama.model = options.model;
32
+ }
33
+
34
+ // Headless/print mode
35
+ if (options.print || options.headless) {
36
+ const { runHeadless } = await import("./headless.js");
37
+ await runHeadless(prompt, config);
38
+ return;
39
+ }
40
+
41
+ // Interactive TUI mode
42
+ const { waitUntilExit } = render(
43
+ React.createElement(App, {
44
+ initialPrompt: prompt,
45
+ config,
46
+ resume: options.resume,
47
+ })
48
+ );
49
+
50
+ await waitUntilExit();
51
+ });
52
+
53
+ program.parse();
@@ -0,0 +1,235 @@
1
+ import { OllamaClient, ChatMessage } from "./llm/ollama.js";
2
+ import { Config } from "./config.js";
3
+ import { getAllTools, executeTool, ToolCall, ToolResult, Tool } from "./tools/index.js";
4
+
5
+ export interface AgentEvent {
6
+ type: "thinking" | "content" | "tool_use" | "tool_result" | "done" | "error";
7
+ content?: string;
8
+ tool?: string;
9
+ status?: "start" | "complete" | "error";
10
+ args?: Record<string, unknown>;
11
+ result?: ToolResult;
12
+ error?: string;
13
+ }
14
+
15
+ export interface AgentResult {
16
+ content: string;
17
+ toolCalls: Array<{
18
+ tool: string;
19
+ args: Record<string, unknown>;
20
+ result: ToolResult;
21
+ }>;
22
+ }
23
+
24
+ const SYSTEM_PROMPT = `You are ACTIVO, an AI-powered code quality analyzer.
25
+
26
+ You help developers:
27
+ 1. Analyze code quality and find issues
28
+ 2. Import development standards from PDFs
29
+ 3. Check code against standards
30
+ 4. Navigate and understand codebases
31
+
32
+ You have access to these tools:
33
+ - read_file: Read file contents
34
+ - write_file: Write content to files
35
+ - list_directory: List directory contents
36
+ - grep_search: Search for patterns in files
37
+ - glob_search: Find files by pattern
38
+ - run_command: Execute shell commands
39
+ - import_pdf_standards: Convert PDF to markdown rules
40
+ - list_standards: List loaded standards
41
+ - check_code_quality: Check code against standards
42
+
43
+ When users ask in natural language, use the appropriate tools to help them.
44
+ Examples:
45
+ - "이 프로젝트 분석해줘" → Use list_directory, read_file to explore
46
+ - "PDF 규칙으로 변환해줘" → Use import_pdf_standards
47
+ - "코드 품질 검사해줘" → Use check_code_quality
48
+ - "UserService 찾아줘" → Use grep_search or glob_search
49
+
50
+ Always explain what you're doing and provide helpful insights.
51
+ Respond in the same language as the user's input.`;
52
+
53
+ export async function processMessage(
54
+ userMessage: string,
55
+ history: ChatMessage[],
56
+ client: OllamaClient,
57
+ config: Config,
58
+ onEvent?: (event: AgentEvent) => void
59
+ ): Promise<AgentResult> {
60
+ const tools = getAllTools();
61
+ const toolDefinitions = tools.map((t) => ({
62
+ name: t.name,
63
+ description: t.description,
64
+ parameters: t.parameters,
65
+ }));
66
+
67
+ const messages: ChatMessage[] = [
68
+ { role: "system", content: SYSTEM_PROMPT },
69
+ ...history,
70
+ { role: "user", content: userMessage },
71
+ ];
72
+
73
+ const toolCallResults: AgentResult["toolCalls"] = [];
74
+ let finalContent = "";
75
+ let iterations = 0;
76
+ const maxIterations = 10;
77
+
78
+ while (iterations < maxIterations) {
79
+ iterations++;
80
+
81
+ onEvent?.({ type: "thinking" });
82
+
83
+ const response = await client.chat(messages, tools as Tool[]);
84
+ messages.push(response);
85
+
86
+ // If no tool calls, we're done
87
+ if (!response.toolCalls?.length) {
88
+ finalContent = response.content;
89
+ break;
90
+ }
91
+
92
+ // Process tool calls
93
+ for (const toolCall of response.toolCalls) {
94
+ onEvent?.({
95
+ type: "tool_use",
96
+ tool: toolCall.name,
97
+ status: "start",
98
+ args: toolCall.arguments,
99
+ });
100
+
101
+ const result = await executeTool(toolCall);
102
+
103
+ onEvent?.({
104
+ type: "tool_result",
105
+ tool: toolCall.name,
106
+ status: result.success ? "complete" : "error",
107
+ result,
108
+ });
109
+
110
+ toolCallResults.push({
111
+ tool: toolCall.name,
112
+ args: toolCall.arguments,
113
+ result,
114
+ });
115
+
116
+ // Add tool result to messages
117
+ messages.push({
118
+ role: "tool",
119
+ content: result.success ? result.content : `Error: ${result.error}`,
120
+ toolCallId: toolCall.id,
121
+ });
122
+ }
123
+
124
+ // Continue the conversation with tool results
125
+ onEvent?.({ type: "content", content: response.content });
126
+ }
127
+
128
+ if (iterations >= maxIterations) {
129
+ onEvent?.({ type: "error", error: "Maximum iterations reached" });
130
+ }
131
+
132
+ onEvent?.({ type: "done" });
133
+
134
+ return {
135
+ content: finalContent,
136
+ toolCalls: toolCallResults,
137
+ };
138
+ }
139
+
140
+ export async function* streamProcessMessage(
141
+ userMessage: string,
142
+ history: ChatMessage[],
143
+ client: OllamaClient,
144
+ config: Config,
145
+ abortSignal?: AbortSignal
146
+ ): AsyncGenerator<AgentEvent> {
147
+ const tools = getAllTools();
148
+
149
+ const messages: ChatMessage[] = [
150
+ { role: "system", content: SYSTEM_PROMPT },
151
+ ...history,
152
+ { role: "user", content: userMessage },
153
+ ];
154
+
155
+ let iterations = 0;
156
+ const maxIterations = 10;
157
+
158
+ while (iterations < maxIterations) {
159
+ // Check if aborted
160
+ if (abortSignal?.aborted) {
161
+ yield { type: "error", error: "Operation cancelled" };
162
+ return;
163
+ }
164
+
165
+ iterations++;
166
+
167
+ yield { type: "thinking" };
168
+
169
+ let fullContent = "";
170
+ const pendingToolCalls: ToolCall[] = [];
171
+
172
+ for await (const event of client.streamChat(messages, tools as Tool[], abortSignal)) {
173
+ // Check if aborted during streaming
174
+ if (abortSignal?.aborted) {
175
+ yield { type: "error", error: "Operation cancelled" };
176
+ return;
177
+ }
178
+ if (event.type === "content" && event.content) {
179
+ fullContent += event.content;
180
+ yield { type: "content", content: event.content };
181
+ } else if (event.type === "tool_call" && event.toolCall) {
182
+ pendingToolCalls.push(event.toolCall);
183
+ } else if (event.type === "error") {
184
+ yield { type: "error", error: event.error };
185
+ return;
186
+ }
187
+ }
188
+
189
+ messages.push({ role: "assistant", content: fullContent, toolCalls: pendingToolCalls.length > 0 ? pendingToolCalls : undefined });
190
+
191
+ // If no tool calls, we're done
192
+ if (pendingToolCalls.length === 0) {
193
+ break;
194
+ }
195
+
196
+ // Process tool calls
197
+ for (const toolCall of pendingToolCalls) {
198
+ // Check if aborted before each tool call
199
+ if (abortSignal?.aborted) {
200
+ yield { type: "error", error: "Operation cancelled" };
201
+ return;
202
+ }
203
+
204
+ yield {
205
+ type: "tool_use",
206
+ tool: toolCall.name,
207
+ status: "start",
208
+ args: toolCall.arguments,
209
+ };
210
+
211
+ const result = await executeTool(toolCall);
212
+
213
+ // Check if aborted after tool execution
214
+ if (abortSignal?.aborted) {
215
+ yield { type: "error", error: "Operation cancelled" };
216
+ return;
217
+ }
218
+
219
+ yield {
220
+ type: "tool_result",
221
+ tool: toolCall.name,
222
+ status: result.success ? "complete" : "error",
223
+ result,
224
+ };
225
+
226
+ messages.push({
227
+ role: "tool",
228
+ content: result.success ? result.content : `Error: ${result.error}`,
229
+ toolCallId: toolCall.id,
230
+ });
231
+ }
232
+ }
233
+
234
+ yield { type: "done" };
235
+ }
@@ -0,0 +1,98 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ export interface OllamaConfig {
6
+ baseUrl: string;
7
+ model: string;
8
+ contextLength: number;
9
+ keepAlive: number;
10
+ }
11
+
12
+ export interface Config {
13
+ ollama: OllamaConfig;
14
+ standards: {
15
+ directory: string;
16
+ };
17
+ mcp: {
18
+ servers: Record<string, MCPServerConfig>;
19
+ };
20
+ }
21
+
22
+ export interface MCPServerConfig {
23
+ command: string;
24
+ args?: string[];
25
+ env?: Record<string, string>;
26
+ }
27
+
28
+ const DEFAULT_CONFIG: Config = {
29
+ ollama: {
30
+ baseUrl: "http://localhost:11434",
31
+ model: "mistral:latest",
32
+ contextLength: 8192,
33
+ keepAlive: 1800, // 30 minutes
34
+ },
35
+ standards: {
36
+ directory: ".activo/standards",
37
+ },
38
+ mcp: {
39
+ servers: {},
40
+ },
41
+ };
42
+
43
+ function getConfigPath(): string {
44
+ return path.join(os.homedir(), ".activo", "config.json");
45
+ }
46
+
47
+ export function loadConfig(): Config {
48
+ const configPath = getConfigPath();
49
+
50
+ if (fs.existsSync(configPath)) {
51
+ try {
52
+ const data = fs.readFileSync(configPath, "utf-8");
53
+ const loaded = JSON.parse(data);
54
+ return {
55
+ ollama: { ...DEFAULT_CONFIG.ollama, ...loaded.ollama },
56
+ standards: { ...DEFAULT_CONFIG.standards, ...loaded.standards },
57
+ mcp: { ...DEFAULT_CONFIG.mcp, ...loaded.mcp },
58
+ };
59
+ } catch {
60
+ return DEFAULT_CONFIG;
61
+ }
62
+ }
63
+
64
+ return DEFAULT_CONFIG;
65
+ }
66
+
67
+ export function saveConfig(config: Config): void {
68
+ const configPath = getConfigPath();
69
+ const configDir = path.dirname(configPath);
70
+
71
+ if (!fs.existsSync(configDir)) {
72
+ fs.mkdirSync(configDir, { recursive: true });
73
+ }
74
+
75
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
76
+ }
77
+
78
+ export function getProjectConfig(): Config {
79
+ const projectConfigPath = path.join(process.cwd(), ".activo", "config.json");
80
+
81
+ if (fs.existsSync(projectConfigPath)) {
82
+ try {
83
+ const data = fs.readFileSync(projectConfigPath, "utf-8");
84
+ const projectConfig = JSON.parse(data);
85
+ const globalConfig = loadConfig();
86
+
87
+ return {
88
+ ollama: { ...globalConfig.ollama, ...projectConfig.ollama },
89
+ standards: { ...globalConfig.standards, ...projectConfig.standards },
90
+ mcp: { ...globalConfig.mcp, ...projectConfig.mcp },
91
+ };
92
+ } catch {
93
+ return loadConfig();
94
+ }
95
+ }
96
+
97
+ return loadConfig();
98
+ }