mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
feat(ipuaro): implement v0.1.0 foundation
- Project setup with tsup, vitest, ESM support - Domain entities: Session, Project - Value objects: FileData, FileAST, FileMeta, ChatMessage, ToolCall, ToolResult, UndoEntry - Service interfaces: IStorage, ILLMClient, ITool, IIndexer, IToolRegistry - Shared: Config (zod), IpuaroError, utils (hash, tokens), Result type - CLI with placeholder commands (start, init, index) - 91 unit tests with 100% coverage - Fix package scope @puaros -> @samiyev in CLAUDE.md
This commit is contained in:
2
packages/ipuaro/src/shared/config/index.ts
Normal file
2
packages/ipuaro/src/shared/config/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Config module exports
|
||||
export * from "./loader.js"
|
||||
89
packages/ipuaro/src/shared/config/loader.ts
Normal file
89
packages/ipuaro/src/shared/config/loader.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { Config, ConfigSchema, DEFAULT_CONFIG } from "../constants/config.js"
|
||||
|
||||
const CONFIG_FILE_NAME = ".ipuaro.json"
|
||||
const DEFAULT_CONFIG_PATH = "config/default.json"
|
||||
|
||||
/**
|
||||
* Load configuration from files.
|
||||
* Priority: .ipuaro.json > config/default.json > defaults
|
||||
*/
|
||||
export function loadConfig(projectRoot: string): Config {
|
||||
const configs: Partial<Config>[] = []
|
||||
|
||||
const defaultConfigPath = join(projectRoot, DEFAULT_CONFIG_PATH)
|
||||
if (existsSync(defaultConfigPath)) {
|
||||
try {
|
||||
const content = readFileSync(defaultConfigPath, "utf-8")
|
||||
configs.push(JSON.parse(content) as Partial<Config>)
|
||||
} catch {
|
||||
// Ignore parse errors for default config
|
||||
}
|
||||
}
|
||||
|
||||
const projectConfigPath = join(projectRoot, CONFIG_FILE_NAME)
|
||||
if (existsSync(projectConfigPath)) {
|
||||
try {
|
||||
const content = readFileSync(projectConfigPath, "utf-8")
|
||||
configs.push(JSON.parse(content) as Partial<Config>)
|
||||
} catch {
|
||||
// Ignore parse errors for project config
|
||||
}
|
||||
}
|
||||
|
||||
if (configs.length === 0) {
|
||||
return DEFAULT_CONFIG
|
||||
}
|
||||
|
||||
const merged = deepMerge(DEFAULT_CONFIG, ...configs)
|
||||
return ConfigSchema.parse(merged)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge objects.
|
||||
*/
|
||||
function deepMerge<T extends Record<string, unknown>>(target: T, ...sources: Partial<T>[]): T {
|
||||
const result = { ...target }
|
||||
|
||||
for (const source of sources) {
|
||||
for (const key in source) {
|
||||
const sourceValue = source[key]
|
||||
const targetValue = result[key]
|
||||
|
||||
if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
|
||||
result[key] = deepMerge(
|
||||
targetValue as Record<string, unknown>,
|
||||
sourceValue as Record<string, unknown>,
|
||||
) as T[Extract<keyof T, string>]
|
||||
} else if (sourceValue !== undefined) {
|
||||
result[key] = sourceValue as T[Extract<keyof T, string>]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration.
|
||||
*/
|
||||
export function validateConfig(config: unknown): config is Config {
|
||||
const result = ConfigSchema.safeParse(config)
|
||||
return result.success
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config validation errors.
|
||||
*/
|
||||
export function getConfigErrors(config: unknown): string[] {
|
||||
const result = ConfigSchema.safeParse(config)
|
||||
if (result.success) {
|
||||
return []
|
||||
}
|
||||
return result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`)
|
||||
}
|
||||
107
packages/ipuaro/src/shared/constants/config.ts
Normal file
107
packages/ipuaro/src/shared/constants/config.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* Redis configuration schema.
|
||||
*/
|
||||
export const RedisConfigSchema = z.object({
|
||||
host: z.string().default("localhost"),
|
||||
port: z.number().int().positive().default(6379),
|
||||
db: z.number().int().min(0).max(15).default(0),
|
||||
password: z.string().optional(),
|
||||
keyPrefix: z.string().default("ipuaro:"),
|
||||
})
|
||||
|
||||
/**
|
||||
* LLM configuration schema.
|
||||
*/
|
||||
export const LLMConfigSchema = z.object({
|
||||
model: z.string().default("qwen2.5-coder:7b-instruct"),
|
||||
contextWindow: z.number().int().positive().default(128_000),
|
||||
temperature: z.number().min(0).max(2).default(0.1),
|
||||
host: z.string().default("http://localhost:11434"),
|
||||
timeout: z.number().int().positive().default(120_000),
|
||||
})
|
||||
|
||||
/**
|
||||
* Project configuration schema.
|
||||
*/
|
||||
export const ProjectConfigSchema = z.object({
|
||||
ignorePatterns: z
|
||||
.array(z.string())
|
||||
.default(["node_modules", "dist", "build", ".git", ".next", ".nuxt", "coverage", ".cache"]),
|
||||
binaryExtensions: z
|
||||
.array(z.string())
|
||||
.default([
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".ico",
|
||||
".svg",
|
||||
".woff",
|
||||
".woff2",
|
||||
".ttf",
|
||||
".eot",
|
||||
".mp3",
|
||||
".mp4",
|
||||
".webm",
|
||||
".pdf",
|
||||
".zip",
|
||||
".tar",
|
||||
".gz",
|
||||
]),
|
||||
maxFileSize: z.number().int().positive().default(1_000_000),
|
||||
supportedExtensions: z
|
||||
.array(z.string())
|
||||
.default([".ts", ".tsx", ".js", ".jsx", ".json", ".yaml", ".yml"]),
|
||||
})
|
||||
|
||||
/**
|
||||
* Watchdog configuration schema.
|
||||
*/
|
||||
export const WatchdogConfigSchema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
debounceMs: z.number().int().positive().default(500),
|
||||
})
|
||||
|
||||
/**
|
||||
* Undo configuration schema.
|
||||
*/
|
||||
export const UndoConfigSchema = z.object({
|
||||
stackSize: z.number().int().positive().default(10),
|
||||
})
|
||||
|
||||
/**
|
||||
* Edit configuration schema.
|
||||
*/
|
||||
export const EditConfigSchema = z.object({
|
||||
autoApply: z.boolean().default(false),
|
||||
})
|
||||
|
||||
/**
|
||||
* Full configuration schema.
|
||||
*/
|
||||
export const ConfigSchema = z.object({
|
||||
redis: RedisConfigSchema.default({}),
|
||||
llm: LLMConfigSchema.default({}),
|
||||
project: ProjectConfigSchema.default({}),
|
||||
watchdog: WatchdogConfigSchema.default({}),
|
||||
undo: UndoConfigSchema.default({}),
|
||||
edit: EditConfigSchema.default({}),
|
||||
})
|
||||
|
||||
/**
|
||||
* Configuration type inferred from schema.
|
||||
*/
|
||||
export type Config = z.infer<typeof ConfigSchema>
|
||||
export type RedisConfig = z.infer<typeof RedisConfigSchema>
|
||||
export type LLMConfig = z.infer<typeof LLMConfigSchema>
|
||||
export type ProjectConfig = z.infer<typeof ProjectConfigSchema>
|
||||
export type WatchdogConfig = z.infer<typeof WatchdogConfigSchema>
|
||||
export type UndoConfig = z.infer<typeof UndoConfigSchema>
|
||||
export type EditConfig = z.infer<typeof EditConfigSchema>
|
||||
|
||||
/**
|
||||
* Default configuration.
|
||||
*/
|
||||
export const DEFAULT_CONFIG: Config = ConfigSchema.parse({})
|
||||
3
packages/ipuaro/src/shared/constants/index.ts
Normal file
3
packages/ipuaro/src/shared/constants/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Shared constants
|
||||
export * from "./config.js"
|
||||
export * from "./messages.js"
|
||||
56
packages/ipuaro/src/shared/constants/messages.ts
Normal file
56
packages/ipuaro/src/shared/constants/messages.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* User-facing messages and labels.
|
||||
*/
|
||||
|
||||
export const MESSAGES = {
|
||||
// Status messages
|
||||
STATUS_READY: "Ready",
|
||||
STATUS_THINKING: "Thinking...",
|
||||
STATUS_INDEXING: "Indexing...",
|
||||
STATUS_ERROR: "Error",
|
||||
|
||||
// Error messages
|
||||
ERROR_REDIS_UNAVAILABLE: "Redis is not available. Please start Redis server.",
|
||||
ERROR_OLLAMA_UNAVAILABLE: "Ollama is not available. Please start Ollama.",
|
||||
ERROR_MODEL_NOT_FOUND: "Model not found. Would you like to pull it?",
|
||||
ERROR_FILE_NOT_FOUND: "File not found",
|
||||
ERROR_PARSE_FAILED: "Failed to parse file",
|
||||
ERROR_TOOL_FAILED: "Tool execution failed",
|
||||
ERROR_COMMAND_BLACKLISTED: "Command is blacklisted for security reasons",
|
||||
ERROR_PATH_OUTSIDE_PROJECT: "Path is outside project directory",
|
||||
|
||||
// Confirmation messages
|
||||
CONFIRM_APPLY_EDIT: "Apply this edit?",
|
||||
CONFIRM_DELETE_FILE: "Delete this file?",
|
||||
CONFIRM_RUN_COMMAND: "Run this command?",
|
||||
CONFIRM_CREATE_FILE: "Create this file?",
|
||||
CONFIRM_GIT_COMMIT: "Create this commit?",
|
||||
|
||||
// Info messages
|
||||
INFO_SESSION_LOADED: "Session loaded",
|
||||
INFO_SESSION_CREATED: "New session created",
|
||||
INFO_INDEXING_COMPLETE: "Indexing complete",
|
||||
INFO_EDIT_APPLIED: "Edit applied",
|
||||
INFO_EDIT_CANCELLED: "Edit cancelled",
|
||||
INFO_UNDO_SUCCESS: "Change reverted",
|
||||
INFO_UNDO_EMPTY: "Nothing to undo",
|
||||
|
||||
// Help text
|
||||
HELP_COMMANDS: `Available commands:
|
||||
/help - Show this help
|
||||
/clear - Clear chat history
|
||||
/undo - Revert last file change
|
||||
/sessions - Manage sessions
|
||||
/status - Show status info
|
||||
/reindex - Force reindexing
|
||||
/auto-apply - Toggle auto-apply mode`,
|
||||
|
||||
HELP_HOTKEYS: `Hotkeys:
|
||||
Ctrl+C - Interrupt / Exit
|
||||
Ctrl+D - Exit with save
|
||||
Ctrl+Z - Undo last change
|
||||
↑/↓ - Navigate history
|
||||
Tab - Autocomplete paths`,
|
||||
} as const
|
||||
|
||||
export type MessageKey = keyof typeof MESSAGES
|
||||
78
packages/ipuaro/src/shared/errors/IpuaroError.ts
Normal file
78
packages/ipuaro/src/shared/errors/IpuaroError.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Error types for ipuaro.
|
||||
*/
|
||||
export type ErrorType =
|
||||
| "redis"
|
||||
| "parse"
|
||||
| "llm"
|
||||
| "file"
|
||||
| "command"
|
||||
| "conflict"
|
||||
| "validation"
|
||||
| "timeout"
|
||||
| "unknown"
|
||||
|
||||
/**
|
||||
* Base error class for ipuaro.
|
||||
*/
|
||||
export class IpuaroError extends Error {
|
||||
readonly type: ErrorType
|
||||
readonly recoverable: boolean
|
||||
readonly suggestion?: string
|
||||
|
||||
constructor(type: ErrorType, message: string, recoverable = true, suggestion?: string) {
|
||||
super(message)
|
||||
this.name = "IpuaroError"
|
||||
this.type = type
|
||||
this.recoverable = recoverable
|
||||
this.suggestion = suggestion
|
||||
}
|
||||
|
||||
static redis(message: string): IpuaroError {
|
||||
return new IpuaroError(
|
||||
"redis",
|
||||
message,
|
||||
false,
|
||||
"Please ensure Redis is running: redis-server",
|
||||
)
|
||||
}
|
||||
|
||||
static parse(message: string, filePath?: string): IpuaroError {
|
||||
const msg = filePath ? `${message} in ${filePath}` : message
|
||||
return new IpuaroError("parse", msg, true, "File will be skipped")
|
||||
}
|
||||
|
||||
static llm(message: string): IpuaroError {
|
||||
return new IpuaroError(
|
||||
"llm",
|
||||
message,
|
||||
true,
|
||||
"Please ensure Ollama is running and model is available",
|
||||
)
|
||||
}
|
||||
|
||||
static file(message: string): IpuaroError {
|
||||
return new IpuaroError("file", message, true)
|
||||
}
|
||||
|
||||
static command(message: string): IpuaroError {
|
||||
return new IpuaroError("command", message, true)
|
||||
}
|
||||
|
||||
static conflict(message: string): IpuaroError {
|
||||
return new IpuaroError(
|
||||
"conflict",
|
||||
message,
|
||||
true,
|
||||
"File was modified externally. Regenerate or skip.",
|
||||
)
|
||||
}
|
||||
|
||||
static validation(message: string): IpuaroError {
|
||||
return new IpuaroError("validation", message, true)
|
||||
}
|
||||
|
||||
static timeout(message: string): IpuaroError {
|
||||
return new IpuaroError("timeout", message, true, "Try again or increase timeout")
|
||||
}
|
||||
}
|
||||
2
packages/ipuaro/src/shared/errors/index.ts
Normal file
2
packages/ipuaro/src/shared/errors/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Shared errors
|
||||
export * from "./IpuaroError.js"
|
||||
6
packages/ipuaro/src/shared/index.ts
Normal file
6
packages/ipuaro/src/shared/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Shared module exports
|
||||
export * from "./config/index.js"
|
||||
export * from "./constants/index.js"
|
||||
export * from "./errors/index.js"
|
||||
export * from "./types/index.js"
|
||||
export * from "./utils/index.js"
|
||||
66
packages/ipuaro/src/shared/types/index.ts
Normal file
66
packages/ipuaro/src/shared/types/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Shared types for ipuaro.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Application status.
|
||||
*/
|
||||
export type AppStatus = "ready" | "thinking" | "indexing" | "error"
|
||||
|
||||
/**
|
||||
* File language type.
|
||||
*/
|
||||
export type FileLanguage = "ts" | "tsx" | "js" | "jsx" | "json" | "yaml" | "unknown"
|
||||
|
||||
/**
|
||||
* User choice for confirmations.
|
||||
*/
|
||||
export type ConfirmChoice = "apply" | "cancel" | "edit"
|
||||
|
||||
/**
|
||||
* User choice for errors.
|
||||
*/
|
||||
export type ErrorChoice = "retry" | "skip" | "abort"
|
||||
|
||||
/**
|
||||
* Project structure node.
|
||||
*/
|
||||
export interface ProjectNode {
|
||||
name: string
|
||||
type: "file" | "directory"
|
||||
path: string
|
||||
children?: ProjectNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic result type.
|
||||
*/
|
||||
export type Result<T, E = Error> = { success: true; data: T } | { success: false; error: E }
|
||||
|
||||
/**
|
||||
* Create success result.
|
||||
*/
|
||||
export function ok<T>(data: T): Result<T, never> {
|
||||
return { success: true, data }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create error result.
|
||||
*/
|
||||
export function err<E>(error: E): Result<never, E> {
|
||||
return { success: false, error }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if result is success.
|
||||
*/
|
||||
export function isOk<T, E>(result: Result<T, E>): result is { success: true; data: T } {
|
||||
return result.success
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if result is error.
|
||||
*/
|
||||
export function isErr<T, E>(result: Result<T, E>): result is { success: false; error: E } {
|
||||
return !result.success
|
||||
}
|
||||
22
packages/ipuaro/src/shared/utils/hash.ts
Normal file
22
packages/ipuaro/src/shared/utils/hash.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createHash } from "node:crypto"
|
||||
|
||||
/**
|
||||
* Calculate MD5 hash of content.
|
||||
*/
|
||||
export function md5(content: string): string {
|
||||
return createHash("md5").update(content).digest("hex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate MD5 hash of file lines.
|
||||
*/
|
||||
export function hashLines(lines: string[]): string {
|
||||
return md5(lines.join("\n"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate short hash for IDs.
|
||||
*/
|
||||
export function shortHash(content: string, length = 8): string {
|
||||
return md5(content).slice(0, length)
|
||||
}
|
||||
3
packages/ipuaro/src/shared/utils/index.ts
Normal file
3
packages/ipuaro/src/shared/utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Shared utilities
|
||||
export * from "./hash.js"
|
||||
export * from "./tokens.js"
|
||||
41
packages/ipuaro/src/shared/utils/tokens.ts
Normal file
41
packages/ipuaro/src/shared/utils/tokens.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Simple token estimation utilities.
|
||||
* Uses approximation: ~4 characters per token for English text.
|
||||
*/
|
||||
|
||||
const CHARS_PER_TOKEN = 4
|
||||
|
||||
/**
|
||||
* Estimate token count for text.
|
||||
*/
|
||||
export function estimateTokens(text: string): number {
|
||||
return Math.ceil(text.length / CHARS_PER_TOKEN)
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate token count for array of strings.
|
||||
*/
|
||||
export function estimateTokensForLines(lines: string[]): number {
|
||||
return estimateTokens(lines.join("\n"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text to approximate token limit.
|
||||
*/
|
||||
export function truncateToTokens(text: string, maxTokens: number): string {
|
||||
const maxChars = maxTokens * CHARS_PER_TOKEN
|
||||
if (text.length <= maxChars) {
|
||||
return text
|
||||
}
|
||||
return `${text.slice(0, maxChars)}...`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format token count for display.
|
||||
*/
|
||||
export function formatTokenCount(tokens: number): string {
|
||||
if (tokens >= 1000) {
|
||||
return `${(tokens / 1000).toFixed(1)}k`
|
||||
}
|
||||
return tokens.toString()
|
||||
}
|
||||
Reference in New Issue
Block a user