Terminal-based AI coding agent with interactive TUI for autonomous code generation.

Features:
  - Interactive TUI with React/Ink
  - Autonomous agent with tool calls (bash, read, write, edit, glob, grep)
  - Permission system with pattern-based rules
  - Session management with auto-compaction
  - Dual providers: GitHub Copilot and Ollama
  - MCP server integration
  - Todo panel and theme system
  - Streaming responses
  - GitHub-compatible project context
This commit is contained in:
2026-01-27 23:33:06 -05:00
commit 0062e5d9d9
521 changed files with 66418 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
/**
* File change analysis for command suggestions
*/
import { basename } from "path";
import { detectProjectContext } from "@services/command-suggestion/context";
import { SUGGESTION_PATTERNS } from "@services/command-suggestion/patterns";
import {
getProjectContext,
setProjectContext,
addSuggestion,
} from "@services/command-suggestion/state";
import type {
CommandSuggestion,
SuggestionPattern,
} from "@/types/command-suggestion";
const matchesFilePattern = (
pattern: SuggestionPattern,
filePath: string,
fileName: string,
): boolean =>
pattern.filePatterns.some((p) => p.test(filePath) || p.test(fileName));
const matchesContentPattern = (
pattern: SuggestionPattern,
content?: string,
): boolean => {
if (!pattern.contentPatterns || !content) {
return true;
}
return pattern.contentPatterns.some((p) => p.test(content));
};
export const analyzeFileChange = (
filePath: string,
content?: string,
): CommandSuggestion[] => {
let ctx = getProjectContext();
if (!ctx) {
ctx = detectProjectContext(process.cwd());
setProjectContext(ctx);
}
const newSuggestions: CommandSuggestion[] = [];
const fileName = basename(filePath);
for (const pattern of SUGGESTION_PATTERNS) {
if (!matchesFilePattern(pattern, filePath, fileName)) {
continue;
}
if (!matchesContentPattern(pattern, content)) {
continue;
}
const suggestions = pattern.suggestions(ctx, filePath);
for (const suggestion of suggestions) {
if (addSuggestion(suggestion)) {
newSuggestions.push(suggestion);
}
}
}
return newSuggestions;
};

View File

@@ -0,0 +1,41 @@
/**
* Project context detection
*/
import { existsSync } from "fs";
import { join } from "path";
import { PROJECT_FILES } from "@constants/command-suggestion";
import type { ProjectContext } from "@/types/command-suggestion";
export const detectProjectContext = (cwd: string): ProjectContext => ({
hasPackageJson: existsSync(join(cwd, PROJECT_FILES.PACKAGE_JSON)),
hasYarnLock: existsSync(join(cwd, PROJECT_FILES.YARN_LOCK)),
hasPnpmLock: existsSync(join(cwd, PROJECT_FILES.PNPM_LOCK)),
hasBunLock: existsSync(join(cwd, PROJECT_FILES.BUN_LOCK)),
hasCargoToml: existsSync(join(cwd, PROJECT_FILES.CARGO_TOML)),
hasGoMod: existsSync(join(cwd, PROJECT_FILES.GO_MOD)),
hasPyproject: existsSync(join(cwd, PROJECT_FILES.PYPROJECT)),
hasRequirements: existsSync(join(cwd, PROJECT_FILES.REQUIREMENTS)),
hasMakefile: existsSync(join(cwd, PROJECT_FILES.MAKEFILE)),
hasDockerfile: existsSync(join(cwd, PROJECT_FILES.DOCKERFILE)),
cwd,
});
const PACKAGE_MANAGER_PRIORITY: Array<{
check: (ctx: ProjectContext) => boolean;
manager: string;
}> = [
{ check: (ctx) => ctx.hasBunLock, manager: "bun" },
{ check: (ctx) => ctx.hasPnpmLock, manager: "pnpm" },
{ check: (ctx) => ctx.hasYarnLock, manager: "yarn" },
];
export const getPackageManager = (ctx: ProjectContext): string => {
for (const { check, manager } of PACKAGE_MANAGER_PRIORITY) {
if (check(ctx)) {
return manager;
}
}
return "npm";
};

View File

@@ -0,0 +1,41 @@
/**
* Command suggestion formatting and retrieval
*/
import { PRIORITY_ORDER, PRIORITY_ICONS } from "@constants/command-suggestion";
import {
getPendingSuggestionsMap,
clearSuggestions as clearStore,
removeSuggestion as removeFromStore,
} from "@services/command-suggestion/state";
import type { CommandSuggestion } from "@/types/command-suggestion";
export const getPendingSuggestions = (): CommandSuggestion[] => {
const suggestionsMap = getPendingSuggestionsMap();
return Array.from(suggestionsMap.values()).sort(
(a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority],
);
};
export const clearSuggestions = (): void => clearStore();
export const removeSuggestion = (command: string): void =>
removeFromStore(command);
export const formatSuggestions = (suggestions: CommandSuggestion[]): string => {
if (suggestions.length === 0) return "";
const lines = ["", "Suggested commands:"];
for (const s of suggestions) {
const icon = PRIORITY_ICONS[s.priority];
lines.push(` ${icon} ${s.command} (${s.reason})`);
}
return lines.join("\n");
};
export const hasHighPrioritySuggestions = (): boolean => {
const suggestionsMap = getPendingSuggestionsMap();
return Array.from(suggestionsMap.values()).some((s) => s.priority === "high");
};

View File

@@ -0,0 +1,225 @@
/**
* Command suggestion patterns
*/
import {
FILE_PATTERNS,
CONTENT_PATTERNS,
SUGGESTION_MESSAGES,
SUGGESTION_REASONS,
} from "@constants/command-suggestion";
import { getPackageManager } from "@services/command-suggestion/context";
import type {
SuggestionPattern,
ProjectContext,
CommandSuggestion,
} from "@/types/command-suggestion";
const createPackageJsonSuggestions = (
ctx: ProjectContext,
): CommandSuggestion[] => {
const pm = getPackageManager(ctx);
return [
{
command: `${pm} install`,
description: SUGGESTION_MESSAGES.INSTALL_DEPS,
priority: "high",
reason: SUGGESTION_REASONS.PACKAGE_JSON_MODIFIED,
},
];
};
const createTsconfigSuggestions = (
ctx: ProjectContext,
): CommandSuggestion[] => {
const pm = getPackageManager(ctx);
return [
{
command: `${pm} run build`,
description: SUGGESTION_MESSAGES.REBUILD_PROJECT,
priority: "medium",
reason: SUGGESTION_REASONS.TSCONFIG_CHANGED,
},
];
};
const createSourceFileSuggestions = (
ctx: ProjectContext,
filePath: string,
): CommandSuggestion[] => {
const pm = getPackageManager(ctx);
const isTestFile = FILE_PATTERNS.TEST_FILE.test(filePath);
return isTestFile
? [
{
command: `${pm} test`,
description: SUGGESTION_MESSAGES.RUN_TESTS,
priority: "high",
reason: SUGGESTION_REASONS.TEST_FILE_MODIFIED,
},
]
: [
{
command: `${pm} run dev`,
description: SUGGESTION_MESSAGES.START_DEV,
priority: "low",
reason: SUGGESTION_REASONS.SOURCE_FILE_MODIFIED,
},
];
};
const createCargoSuggestions = (): CommandSuggestion[] => [
{
command: "cargo build",
description: SUGGESTION_MESSAGES.BUILD_RUST,
priority: "high",
reason: SUGGESTION_REASONS.CARGO_MODIFIED,
},
];
const createGoModSuggestions = (): CommandSuggestion[] => [
{
command: "go mod tidy",
description: SUGGESTION_MESSAGES.TIDY_GO,
priority: "high",
reason: SUGGESTION_REASONS.GO_MOD_MODIFIED,
},
];
const createPythonSuggestions = (ctx: ProjectContext): CommandSuggestion[] =>
ctx.hasPyproject
? [
{
command: "pip install -e .",
description: SUGGESTION_MESSAGES.INSTALL_PYTHON_EDITABLE,
priority: "high",
reason: SUGGESTION_REASONS.PYTHON_DEPS_CHANGED,
},
]
: [
{
command: "pip install -r requirements.txt",
description: SUGGESTION_MESSAGES.INSTALL_PYTHON_DEPS,
priority: "high",
reason: SUGGESTION_REASONS.REQUIREMENTS_MODIFIED,
},
];
const createDockerSuggestions = (
_ctx: ProjectContext,
filePath: string,
): CommandSuggestion[] =>
filePath.includes("docker-compose")
? [
{
command: "docker-compose up --build",
description: SUGGESTION_MESSAGES.DOCKER_COMPOSE_BUILD,
priority: "medium",
reason: SUGGESTION_REASONS.DOCKER_COMPOSE_CHANGED,
},
]
: [
{
command: "docker build -t app .",
description: SUGGESTION_MESSAGES.DOCKER_BUILD,
priority: "medium",
reason: SUGGESTION_REASONS.DOCKERFILE_MODIFIED,
},
];
const createMakefileSuggestions = (): CommandSuggestion[] => [
{
command: "make",
description: SUGGESTION_MESSAGES.RUN_MAKE,
priority: "medium",
reason: SUGGESTION_REASONS.MAKEFILE_MODIFIED,
},
];
const createMigrationSuggestions = (
ctx: ProjectContext,
): CommandSuggestion[] => {
const pm = getPackageManager(ctx);
return [
{
command: `${pm} run migrate`,
description: SUGGESTION_MESSAGES.RUN_MIGRATE,
priority: "high",
reason: SUGGESTION_REASONS.MIGRATION_MODIFIED,
},
];
};
const createEnvSuggestions = (): CommandSuggestion[] => [
{
command: "cp .env.example .env",
description: SUGGESTION_MESSAGES.CREATE_ENV,
priority: "medium",
reason: SUGGESTION_REASONS.ENV_TEMPLATE_MODIFIED,
},
];
const createLinterSuggestions = (ctx: ProjectContext): CommandSuggestion[] => {
const pm = getPackageManager(ctx);
return [
{
command: `${pm} run lint`,
description: SUGGESTION_MESSAGES.RUN_LINT,
priority: "low",
reason: SUGGESTION_REASONS.LINTER_CONFIG_CHANGED,
},
];
};
export const SUGGESTION_PATTERNS: SuggestionPattern[] = [
{
filePatterns: [FILE_PATTERNS.PACKAGE_JSON],
contentPatterns: [
CONTENT_PATTERNS.DEPENDENCIES,
CONTENT_PATTERNS.DEV_DEPENDENCIES,
CONTENT_PATTERNS.PEER_DEPENDENCIES,
],
suggestions: createPackageJsonSuggestions,
},
{
filePatterns: [FILE_PATTERNS.TSCONFIG],
suggestions: createTsconfigSuggestions,
},
{
filePatterns: [FILE_PATTERNS.SOURCE_FILES],
suggestions: createSourceFileSuggestions,
},
{
filePatterns: [FILE_PATTERNS.CARGO_TOML],
suggestions: createCargoSuggestions,
},
{
filePatterns: [FILE_PATTERNS.GO_MOD],
suggestions: createGoModSuggestions,
},
{
filePatterns: [FILE_PATTERNS.PYTHON_DEPS],
suggestions: createPythonSuggestions,
},
{
filePatterns: [FILE_PATTERNS.DOCKER],
suggestions: createDockerSuggestions,
},
{
filePatterns: [FILE_PATTERNS.MAKEFILE],
suggestions: createMakefileSuggestions,
},
{
filePatterns: [FILE_PATTERNS.MIGRATIONS],
suggestions: createMigrationSuggestions,
},
{
filePatterns: [FILE_PATTERNS.ENV_EXAMPLE],
suggestions: createEnvSuggestions,
},
{
filePatterns: [FILE_PATTERNS.LINTER_CONFIG],
suggestions: createLinterSuggestions,
},
];

View File

@@ -0,0 +1,57 @@
/**
* Command suggestion state management
*/
import { createStore } from "zustand/vanilla";
import type {
CommandSuggestion,
ProjectContext,
} from "@/types/command-suggestion";
interface SuggestionState {
pendingSuggestions: Map<string, CommandSuggestion>;
projectContext: ProjectContext | null;
}
const store = createStore<SuggestionState>(() => ({
pendingSuggestions: new Map(),
projectContext: null,
}));
export const getProjectContext = (): ProjectContext | null =>
store.getState().projectContext;
export const setProjectContext = (ctx: ProjectContext): void => {
store.setState({ projectContext: ctx });
};
export const addSuggestion = (suggestion: CommandSuggestion): boolean => {
const { pendingSuggestions } = store.getState();
if (pendingSuggestions.has(suggestion.command)) {
return false;
}
const newMap = new Map(pendingSuggestions);
newMap.set(suggestion.command, suggestion);
store.setState({ pendingSuggestions: newMap });
return true;
};
export const removeSuggestion = (command: string): void => {
const { pendingSuggestions } = store.getState();
const newMap = new Map(pendingSuggestions);
newMap.delete(command);
store.setState({ pendingSuggestions: newMap });
};
export const clearSuggestions = (): void => {
store.setState({ pendingSuggestions: new Map() });
};
export const getPendingSuggestionsMap = (): Map<string, CommandSuggestion> =>
store.getState().pendingSuggestions;
export const hasSuggestion = (command: string): boolean =>
store.getState().pendingSuggestions.has(command);
export const subscribeToSuggestions = store.subscribe;