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:
4
packages/ipuaro/src/application/dtos/index.ts
Normal file
4
packages/ipuaro/src/application/dtos/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
* Application DTOs
|
||||
* Will be implemented in version 0.10.0+
|
||||
*/
|
||||
10
packages/ipuaro/src/application/index.ts
Normal file
10
packages/ipuaro/src/application/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Application Layer exports
|
||||
|
||||
// Use Cases
|
||||
export * from "./use-cases/index.js"
|
||||
|
||||
// DTOs
|
||||
export * from "./dtos/index.js"
|
||||
|
||||
// Interfaces
|
||||
export * from "./interfaces/index.js"
|
||||
51
packages/ipuaro/src/application/interfaces/IToolRegistry.ts
Normal file
51
packages/ipuaro/src/application/interfaces/IToolRegistry.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { ITool, ToolContext } from "../../domain/services/ITool.js"
|
||||
import type { ToolResult } from "../../domain/value-objects/ToolResult.js"
|
||||
|
||||
/**
|
||||
* Tool registry interface.
|
||||
* Manages registration and execution of tools.
|
||||
*/
|
||||
export interface IToolRegistry {
|
||||
/**
|
||||
* Register a tool.
|
||||
*/
|
||||
register(tool: ITool): void
|
||||
|
||||
/**
|
||||
* Get tool by name.
|
||||
*/
|
||||
get(name: string): ITool | undefined
|
||||
|
||||
/**
|
||||
* Get all registered tools.
|
||||
*/
|
||||
getAll(): ITool[]
|
||||
|
||||
/**
|
||||
* Get tools by category.
|
||||
*/
|
||||
getByCategory(category: ITool["category"]): ITool[]
|
||||
|
||||
/**
|
||||
* Check if tool exists.
|
||||
*/
|
||||
has(name: string): boolean
|
||||
|
||||
/**
|
||||
* Execute tool by name.
|
||||
*/
|
||||
execute(name: string, params: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>
|
||||
|
||||
/**
|
||||
* Get tool definitions for LLM.
|
||||
*/
|
||||
getToolDefinitions(): {
|
||||
name: string
|
||||
description: string
|
||||
parameters: {
|
||||
type: "object"
|
||||
properties: Record<string, { type: string; description: string }>
|
||||
required: string[]
|
||||
}
|
||||
}[]
|
||||
}
|
||||
2
packages/ipuaro/src/application/interfaces/index.ts
Normal file
2
packages/ipuaro/src/application/interfaces/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Application Interfaces
|
||||
export * from "./IToolRegistry.js"
|
||||
4
packages/ipuaro/src/application/use-cases/index.ts
Normal file
4
packages/ipuaro/src/application/use-cases/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
* Application Use Cases
|
||||
* Will be implemented in version 0.10.0+
|
||||
*/
|
||||
44
packages/ipuaro/src/cli/index.ts
Normal file
44
packages/ipuaro/src/cli/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from "commander"
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program
|
||||
.name("ipuaro")
|
||||
.description("Local AI agent for codebase operations with infinite context feeling")
|
||||
.version("0.1.0")
|
||||
|
||||
program
|
||||
.command("start")
|
||||
.description("Start ipuaro TUI in the current directory")
|
||||
.argument("[path]", "Project path", ".")
|
||||
.option("--auto-apply", "Enable auto-apply mode for edits")
|
||||
.option("--model <name>", "Override LLM model", "qwen2.5-coder:7b-instruct")
|
||||
.action((path: string, options: { autoApply?: boolean; model?: string }) => {
|
||||
const model = options.model ?? "default"
|
||||
const autoApply = options.autoApply ?? false
|
||||
console.warn(`Starting ipuaro in ${path}...`)
|
||||
console.warn(`Model: ${model}`)
|
||||
console.warn(`Auto-apply: ${autoApply ? "enabled" : "disabled"}`)
|
||||
console.warn("\nNot implemented yet. Coming in version 0.11.0!")
|
||||
})
|
||||
|
||||
program
|
||||
.command("init")
|
||||
.description("Create .ipuaro.json config file")
|
||||
.action(() => {
|
||||
console.warn("Creating .ipuaro.json...")
|
||||
console.warn("\nNot implemented yet. Coming in version 0.17.0!")
|
||||
})
|
||||
|
||||
program
|
||||
.command("index")
|
||||
.description("Index project without starting TUI")
|
||||
.argument("[path]", "Project path", ".")
|
||||
.action((path: string) => {
|
||||
console.warn(`Indexing ${path}...`)
|
||||
console.warn("\nNot implemented yet. Coming in version 0.3.0!")
|
||||
})
|
||||
|
||||
program.parse()
|
||||
48
packages/ipuaro/src/domain/constants/index.ts
Normal file
48
packages/ipuaro/src/domain/constants/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Domain Constants
|
||||
|
||||
export const MAX_UNDO_STACK_SIZE = 10
|
||||
|
||||
export const SUPPORTED_EXTENSIONS = [
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".json",
|
||||
".yaml",
|
||||
".yml",
|
||||
] as const
|
||||
|
||||
export const BINARY_EXTENSIONS = [
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".ico",
|
||||
".svg",
|
||||
".woff",
|
||||
".woff2",
|
||||
".ttf",
|
||||
".eot",
|
||||
".mp3",
|
||||
".mp4",
|
||||
".webm",
|
||||
".pdf",
|
||||
".zip",
|
||||
".tar",
|
||||
".gz",
|
||||
] as const
|
||||
|
||||
export const DEFAULT_IGNORE_PATTERNS = [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
".git",
|
||||
".next",
|
||||
".nuxt",
|
||||
"coverage",
|
||||
".cache",
|
||||
] as const
|
||||
|
||||
export const CONTEXT_WINDOW_SIZE = 128_000
|
||||
|
||||
export const CONTEXT_COMPRESSION_THRESHOLD = 0.8
|
||||
61
packages/ipuaro/src/domain/entities/Project.ts
Normal file
61
packages/ipuaro/src/domain/entities/Project.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { basename, dirname } from "node:path"
|
||||
|
||||
/**
|
||||
* Project entity representing an indexed codebase.
|
||||
*/
|
||||
export class Project {
|
||||
readonly name: string
|
||||
readonly rootPath: string
|
||||
readonly createdAt: number
|
||||
lastIndexedAt: number | null
|
||||
fileCount: number
|
||||
indexingInProgress: boolean
|
||||
|
||||
constructor(rootPath: string, createdAt?: number) {
|
||||
this.rootPath = rootPath
|
||||
this.name = Project.generateProjectName(rootPath)
|
||||
this.createdAt = createdAt ?? Date.now()
|
||||
this.lastIndexedAt = null
|
||||
this.fileCount = 0
|
||||
this.indexingInProgress = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate project name from path.
|
||||
* Format: {parent-folder}-{project-folder}
|
||||
*/
|
||||
static generateProjectName(rootPath: string): string {
|
||||
const projectFolder = basename(rootPath)
|
||||
const parentFolder = basename(dirname(rootPath))
|
||||
|
||||
if (parentFolder && parentFolder !== ".") {
|
||||
return `${parentFolder}-${projectFolder}`
|
||||
}
|
||||
return projectFolder
|
||||
}
|
||||
|
||||
markIndexingStarted(): void {
|
||||
this.indexingInProgress = true
|
||||
}
|
||||
|
||||
markIndexingCompleted(fileCount: number): void {
|
||||
this.indexingInProgress = false
|
||||
this.lastIndexedAt = Date.now()
|
||||
this.fileCount = fileCount
|
||||
}
|
||||
|
||||
markIndexingFailed(): void {
|
||||
this.indexingInProgress = false
|
||||
}
|
||||
|
||||
isIndexed(): boolean {
|
||||
return this.lastIndexedAt !== null
|
||||
}
|
||||
|
||||
getTimeSinceIndexed(): number | null {
|
||||
if (this.lastIndexedAt === null) {
|
||||
return null
|
||||
}
|
||||
return Date.now() - this.lastIndexedAt
|
||||
}
|
||||
}
|
||||
120
packages/ipuaro/src/domain/entities/Session.ts
Normal file
120
packages/ipuaro/src/domain/entities/Session.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { ChatMessage } from "../value-objects/ChatMessage.js"
|
||||
import type { UndoEntry } from "../value-objects/UndoEntry.js"
|
||||
import { MAX_UNDO_STACK_SIZE } from "../constants/index.js"
|
||||
|
||||
/**
|
||||
* Session statistics.
|
||||
*/
|
||||
export interface SessionStats {
|
||||
/** Total tokens used */
|
||||
totalTokens: number
|
||||
/** Total time in milliseconds */
|
||||
totalTimeMs: number
|
||||
/** Number of tool calls made */
|
||||
toolCalls: number
|
||||
/** Number of edits applied */
|
||||
editsApplied: number
|
||||
/** Number of edits rejected */
|
||||
editsRejected: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Context state for the session.
|
||||
*/
|
||||
export interface ContextState {
|
||||
/** Files currently in context */
|
||||
filesInContext: string[]
|
||||
/** Estimated token usage (0-1) */
|
||||
tokenUsage: number
|
||||
/** Whether compression is needed */
|
||||
needsCompression: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Session entity representing a chat session.
|
||||
*/
|
||||
export class Session {
|
||||
readonly id: string
|
||||
readonly projectName: string
|
||||
readonly createdAt: number
|
||||
lastActivityAt: number
|
||||
history: ChatMessage[]
|
||||
context: ContextState
|
||||
undoStack: UndoEntry[]
|
||||
stats: SessionStats
|
||||
inputHistory: string[]
|
||||
|
||||
constructor(id: string, projectName: string, createdAt?: number) {
|
||||
this.id = id
|
||||
this.projectName = projectName
|
||||
this.createdAt = createdAt ?? Date.now()
|
||||
this.lastActivityAt = this.createdAt
|
||||
this.history = []
|
||||
this.context = {
|
||||
filesInContext: [],
|
||||
tokenUsage: 0,
|
||||
needsCompression: false,
|
||||
}
|
||||
this.undoStack = []
|
||||
this.stats = {
|
||||
totalTokens: 0,
|
||||
totalTimeMs: 0,
|
||||
toolCalls: 0,
|
||||
editsApplied: 0,
|
||||
editsRejected: 0,
|
||||
}
|
||||
this.inputHistory = []
|
||||
}
|
||||
|
||||
addMessage(message: ChatMessage): void {
|
||||
this.history.push(message)
|
||||
this.lastActivityAt = Date.now()
|
||||
|
||||
if (message.stats) {
|
||||
this.stats.totalTokens += message.stats.tokens
|
||||
this.stats.totalTimeMs += message.stats.timeMs
|
||||
this.stats.toolCalls += message.stats.toolCalls
|
||||
}
|
||||
}
|
||||
|
||||
addUndoEntry(entry: UndoEntry): void {
|
||||
this.undoStack.push(entry)
|
||||
if (this.undoStack.length > MAX_UNDO_STACK_SIZE) {
|
||||
this.undoStack.shift()
|
||||
}
|
||||
}
|
||||
|
||||
popUndoEntry(): UndoEntry | undefined {
|
||||
return this.undoStack.pop()
|
||||
}
|
||||
|
||||
addInputToHistory(input: string): void {
|
||||
if (input.trim() && this.inputHistory[this.inputHistory.length - 1] !== input) {
|
||||
this.inputHistory.push(input)
|
||||
}
|
||||
}
|
||||
|
||||
clearHistory(): void {
|
||||
this.history = []
|
||||
this.context = {
|
||||
filesInContext: [],
|
||||
tokenUsage: 0,
|
||||
needsCompression: false,
|
||||
}
|
||||
}
|
||||
|
||||
getSessionDurationMs(): number {
|
||||
return Date.now() - this.createdAt
|
||||
}
|
||||
|
||||
getSessionDurationFormatted(): string {
|
||||
const totalMinutes = Math.floor(this.getSessionDurationMs() / 60_000)
|
||||
const hours = Math.floor(totalMinutes / 60)
|
||||
const minutes = totalMinutes % 60
|
||||
|
||||
if (hours > 0) {
|
||||
return `${String(hours)}h ${String(minutes)}m`
|
||||
}
|
||||
return `${String(minutes)}m`
|
||||
}
|
||||
}
|
||||
3
packages/ipuaro/src/domain/entities/index.ts
Normal file
3
packages/ipuaro/src/domain/entities/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Domain Entities
|
||||
export * from "./Session.js"
|
||||
export * from "./Project.js"
|
||||
13
packages/ipuaro/src/domain/index.ts
Normal file
13
packages/ipuaro/src/domain/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// Domain Layer exports
|
||||
|
||||
// Entities
|
||||
export * from "./entities/index.js"
|
||||
|
||||
// Value Objects
|
||||
export * from "./value-objects/index.js"
|
||||
|
||||
// Service Interfaces
|
||||
export * from "./services/index.js"
|
||||
|
||||
// Constants
|
||||
export * from "./constants/index.js"
|
||||
83
packages/ipuaro/src/domain/services/IIndexer.ts
Normal file
83
packages/ipuaro/src/domain/services/IIndexer.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { FileAST } from "../value-objects/FileAST.js"
|
||||
import type { FileData } from "../value-objects/FileData.js"
|
||||
import type { FileMeta } from "../value-objects/FileMeta.js"
|
||||
import type { DepsGraph, SymbolIndex } from "./IStorage.js"
|
||||
|
||||
/**
|
||||
* Progress callback for indexing operations.
|
||||
*/
|
||||
export interface IndexProgress {
|
||||
current: number
|
||||
total: number
|
||||
currentFile: string
|
||||
phase: "scanning" | "parsing" | "analyzing" | "indexing"
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of scanning a single file.
|
||||
*/
|
||||
export interface ScanResult {
|
||||
path: string
|
||||
type: "file" | "directory" | "symlink"
|
||||
size: number
|
||||
lastModified: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexing result statistics.
|
||||
*/
|
||||
export interface IndexingStats {
|
||||
filesScanned: number
|
||||
filesParsed: number
|
||||
parseErrors: number
|
||||
timeMs: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexer service interface (port).
|
||||
* Handles project scanning, parsing, and indexing.
|
||||
*/
|
||||
export interface IIndexer {
|
||||
/**
|
||||
* Scan directory and yield file results.
|
||||
*/
|
||||
scan(root: string): AsyncGenerator<ScanResult>
|
||||
|
||||
/**
|
||||
* Parse file content into AST.
|
||||
*/
|
||||
parseFile(content: string, language: "ts" | "tsx" | "js" | "jsx"): FileAST
|
||||
|
||||
/**
|
||||
* Analyze file and compute metadata.
|
||||
*/
|
||||
analyzeFile(path: string, ast: FileAST, allASTs: Map<string, FileAST>): FileMeta
|
||||
|
||||
/**
|
||||
* Build symbol index from all ASTs.
|
||||
*/
|
||||
buildSymbolIndex(asts: Map<string, FileAST>): SymbolIndex
|
||||
|
||||
/**
|
||||
* Build dependency graph from all ASTs.
|
||||
*/
|
||||
buildDepsGraph(asts: Map<string, FileAST>): DepsGraph
|
||||
|
||||
/**
|
||||
* Full indexing pipeline.
|
||||
*/
|
||||
indexProject(
|
||||
root: string,
|
||||
onProgress?: (progress: IndexProgress) => void,
|
||||
): Promise<IndexingStats>
|
||||
|
||||
/**
|
||||
* Update single file (incremental indexing).
|
||||
*/
|
||||
updateFile(path: string, data: FileData): Promise<void>
|
||||
|
||||
/**
|
||||
* Remove file from index.
|
||||
*/
|
||||
removeFile(path: string): Promise<void>
|
||||
}
|
||||
81
packages/ipuaro/src/domain/services/ILLMClient.ts
Normal file
81
packages/ipuaro/src/domain/services/ILLMClient.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { ChatMessage } from "../value-objects/ChatMessage.js"
|
||||
import type { ToolCall } from "../value-objects/ToolCall.js"
|
||||
|
||||
/**
|
||||
* Tool parameter definition for LLM.
|
||||
*/
|
||||
export interface ToolParameter {
|
||||
name: string
|
||||
type: "string" | "number" | "boolean" | "array" | "object"
|
||||
description: string
|
||||
required: boolean
|
||||
enum?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool definition for LLM function calling.
|
||||
*/
|
||||
export interface ToolDef {
|
||||
name: string
|
||||
description: string
|
||||
parameters: ToolParameter[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from LLM.
|
||||
*/
|
||||
export interface LLMResponse {
|
||||
/** Text content of the response */
|
||||
content: string
|
||||
/** Tool calls parsed from response */
|
||||
toolCalls: ToolCall[]
|
||||
/** Token count for this response */
|
||||
tokens: number
|
||||
/** Generation time in milliseconds */
|
||||
timeMs: number
|
||||
/** Whether response was truncated */
|
||||
truncated: boolean
|
||||
/** Stop reason */
|
||||
stopReason: "end" | "length" | "tool_use"
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM client service interface (port).
|
||||
* Abstracts the LLM provider.
|
||||
*/
|
||||
export interface ILLMClient {
|
||||
/**
|
||||
* Send messages to LLM and get response.
|
||||
*/
|
||||
chat(messages: ChatMessage[], tools?: ToolDef[]): Promise<LLMResponse>
|
||||
|
||||
/**
|
||||
* Count tokens in text.
|
||||
*/
|
||||
countTokens(text: string): Promise<number>
|
||||
|
||||
/**
|
||||
* Check if LLM service is available.
|
||||
*/
|
||||
isAvailable(): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Get current model name.
|
||||
*/
|
||||
getModelName(): string
|
||||
|
||||
/**
|
||||
* Get context window size.
|
||||
*/
|
||||
getContextWindowSize(): number
|
||||
|
||||
/**
|
||||
* Pull/download model if not available locally.
|
||||
*/
|
||||
pullModel(model: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Abort current generation.
|
||||
*/
|
||||
abort(): void
|
||||
}
|
||||
65
packages/ipuaro/src/domain/services/IStorage.ts
Normal file
65
packages/ipuaro/src/domain/services/IStorage.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { FileData } from "../value-objects/FileData.js"
|
||||
import type { FileAST } from "../value-objects/FileAST.js"
|
||||
import type { FileMeta } from "../value-objects/FileMeta.js"
|
||||
|
||||
/**
|
||||
* Symbol index mapping symbol names to their locations.
|
||||
*/
|
||||
export interface SymbolLocation {
|
||||
path: string
|
||||
line: number
|
||||
type: "function" | "class" | "interface" | "type" | "variable"
|
||||
}
|
||||
|
||||
export type SymbolIndex = Map<string, SymbolLocation[]>
|
||||
|
||||
/**
|
||||
* Dependencies graph for the project.
|
||||
*/
|
||||
export interface DepsGraph {
|
||||
/** Map from file path to its imports */
|
||||
imports: Map<string, string[]>
|
||||
/** Map from file path to files that import it */
|
||||
importedBy: Map<string, string[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage service interface (port).
|
||||
* Abstracts the persistence layer for project data.
|
||||
*/
|
||||
export interface IStorage {
|
||||
// File data operations
|
||||
getFile(path: string): Promise<FileData | null>
|
||||
setFile(path: string, data: FileData): Promise<void>
|
||||
deleteFile(path: string): Promise<void>
|
||||
getAllFiles(): Promise<Map<string, FileData>>
|
||||
getFileCount(): Promise<number>
|
||||
|
||||
// AST operations
|
||||
getAST(path: string): Promise<FileAST | null>
|
||||
setAST(path: string, ast: FileAST): Promise<void>
|
||||
deleteAST(path: string): Promise<void>
|
||||
getAllASTs(): Promise<Map<string, FileAST>>
|
||||
|
||||
// Meta operations
|
||||
getMeta(path: string): Promise<FileMeta | null>
|
||||
setMeta(path: string, meta: FileMeta): Promise<void>
|
||||
deleteMeta(path: string): Promise<void>
|
||||
getAllMetas(): Promise<Map<string, FileMeta>>
|
||||
|
||||
// Index operations
|
||||
getSymbolIndex(): Promise<SymbolIndex>
|
||||
setSymbolIndex(index: SymbolIndex): Promise<void>
|
||||
getDepsGraph(): Promise<DepsGraph>
|
||||
setDepsGraph(graph: DepsGraph): Promise<void>
|
||||
|
||||
// Config operations
|
||||
getProjectConfig(key: string): Promise<unknown>
|
||||
setProjectConfig(key: string, value: unknown): Promise<void>
|
||||
|
||||
// Lifecycle
|
||||
connect(): Promise<void>
|
||||
disconnect(): Promise<void>
|
||||
isConnected(): boolean
|
||||
clear(): Promise<void>
|
||||
}
|
||||
68
packages/ipuaro/src/domain/services/ITool.ts
Normal file
68
packages/ipuaro/src/domain/services/ITool.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { ToolResult } from "../value-objects/ToolResult.js"
|
||||
import type { IStorage } from "./IStorage.js"
|
||||
|
||||
/**
|
||||
* Tool parameter schema.
|
||||
*/
|
||||
export interface ToolParameterSchema {
|
||||
name: string
|
||||
type: "string" | "number" | "boolean" | "array" | "object"
|
||||
description: string
|
||||
required: boolean
|
||||
default?: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Context provided to tools during execution.
|
||||
*/
|
||||
export interface ToolContext {
|
||||
/** Project root path */
|
||||
projectRoot: string
|
||||
/** Storage service */
|
||||
storage: IStorage
|
||||
/** Request user confirmation callback */
|
||||
requestConfirmation: (message: string, diff?: DiffInfo) => Promise<boolean>
|
||||
/** Report progress callback */
|
||||
onProgress?: (message: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff information for confirmation dialogs.
|
||||
*/
|
||||
export interface DiffInfo {
|
||||
filePath: string
|
||||
oldLines: string[]
|
||||
newLines: string[]
|
||||
startLine: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool interface (port).
|
||||
* All tools must implement this interface.
|
||||
*/
|
||||
export interface ITool {
|
||||
/** Tool name (used in tool calls) */
|
||||
readonly name: string
|
||||
|
||||
/** Human-readable description */
|
||||
readonly description: string
|
||||
|
||||
/** Tool parameters schema */
|
||||
readonly parameters: ToolParameterSchema[]
|
||||
|
||||
/** Whether tool requires user confirmation before execution */
|
||||
readonly requiresConfirmation: boolean
|
||||
|
||||
/** Tool category */
|
||||
readonly category: "read" | "edit" | "search" | "analysis" | "git" | "run"
|
||||
|
||||
/**
|
||||
* Execute the tool with given parameters.
|
||||
*/
|
||||
execute(params: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>
|
||||
|
||||
/**
|
||||
* Validate parameters before execution.
|
||||
*/
|
||||
validateParams(params: Record<string, unknown>): string | null
|
||||
}
|
||||
5
packages/ipuaro/src/domain/services/index.ts
Normal file
5
packages/ipuaro/src/domain/services/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Domain Service Interfaces (Ports)
|
||||
export * from "./IStorage.js"
|
||||
export * from "./ILLMClient.js"
|
||||
export * from "./ITool.js"
|
||||
export * from "./IIndexer.js"
|
||||
79
packages/ipuaro/src/domain/value-objects/ChatMessage.ts
Normal file
79
packages/ipuaro/src/domain/value-objects/ChatMessage.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { ToolCall } from "./ToolCall.js"
|
||||
import type { ToolResult } from "./ToolResult.js"
|
||||
|
||||
/**
|
||||
* Represents a message in the chat history.
|
||||
*/
|
||||
|
||||
export type MessageRole = "user" | "assistant" | "tool" | "system"
|
||||
|
||||
export interface MessageStats {
|
||||
/** Token count for this message */
|
||||
tokens: number
|
||||
/** Response generation time in ms (for assistant messages) */
|
||||
timeMs: number
|
||||
/** Number of tool calls in this message */
|
||||
toolCalls: number
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
/** Message role */
|
||||
role: MessageRole
|
||||
/** Message content */
|
||||
content: string
|
||||
/** Timestamp when message was created */
|
||||
timestamp: number
|
||||
/** Tool calls made by assistant (if any) */
|
||||
toolCalls?: ToolCall[]
|
||||
/** Tool results (for tool role messages) */
|
||||
toolResults?: ToolResult[]
|
||||
/** Message statistics */
|
||||
stats?: MessageStats
|
||||
}
|
||||
|
||||
export function createUserMessage(content: string): ChatMessage {
|
||||
return {
|
||||
role: "user",
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
export function createAssistantMessage(
|
||||
content: string,
|
||||
toolCalls?: ToolCall[],
|
||||
stats?: MessageStats,
|
||||
): ChatMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
toolCalls,
|
||||
stats,
|
||||
}
|
||||
}
|
||||
|
||||
export function createToolMessage(results: ToolResult[]): ChatMessage {
|
||||
return {
|
||||
role: "tool",
|
||||
content: results.map((r) => formatToolResult(r)).join("\n\n"),
|
||||
timestamp: Date.now(),
|
||||
toolResults: results,
|
||||
}
|
||||
}
|
||||
|
||||
export function createSystemMessage(content: string): ChatMessage {
|
||||
return {
|
||||
role: "system",
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
function formatToolResult(result: ToolResult): string {
|
||||
if (result.success) {
|
||||
return `[${result.callId}] Success: ${JSON.stringify(result.data)}`
|
||||
}
|
||||
const errorMsg = result.error ?? "Unknown error"
|
||||
return `[${result.callId}] Error: ${errorMsg}`
|
||||
}
|
||||
163
packages/ipuaro/src/domain/value-objects/FileAST.ts
Normal file
163
packages/ipuaro/src/domain/value-objects/FileAST.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Represents parsed AST information for a file.
|
||||
*/
|
||||
|
||||
export interface ImportInfo {
|
||||
/** Import name or alias */
|
||||
name: string
|
||||
/** Source module path */
|
||||
from: string
|
||||
/** Line number of import statement */
|
||||
line: number
|
||||
/** Import type classification */
|
||||
type: "internal" | "external" | "builtin"
|
||||
/** Whether it's a default import */
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
export interface ExportInfo {
|
||||
/** Exported name */
|
||||
name: string
|
||||
/** Line number of export */
|
||||
line: number
|
||||
/** Whether it's a default export */
|
||||
isDefault: boolean
|
||||
/** Export type: function, class, variable, type */
|
||||
kind: "function" | "class" | "variable" | "type" | "interface"
|
||||
}
|
||||
|
||||
export interface ParameterInfo {
|
||||
/** Parameter name */
|
||||
name: string
|
||||
/** Parameter type (if available) */
|
||||
type?: string
|
||||
/** Whether it's optional */
|
||||
optional: boolean
|
||||
/** Whether it has a default value */
|
||||
hasDefault: boolean
|
||||
}
|
||||
|
||||
export interface FunctionInfo {
|
||||
/** Function name */
|
||||
name: string
|
||||
/** Start line number */
|
||||
lineStart: number
|
||||
/** End line number */
|
||||
lineEnd: number
|
||||
/** Function parameters */
|
||||
params: ParameterInfo[]
|
||||
/** Whether function is async */
|
||||
isAsync: boolean
|
||||
/** Whether function is exported */
|
||||
isExported: boolean
|
||||
/** Return type (if available) */
|
||||
returnType?: string
|
||||
}
|
||||
|
||||
export interface MethodInfo {
|
||||
/** Method name */
|
||||
name: string
|
||||
/** Start line number */
|
||||
lineStart: number
|
||||
/** End line number */
|
||||
lineEnd: number
|
||||
/** Method parameters */
|
||||
params: ParameterInfo[]
|
||||
/** Whether method is async */
|
||||
isAsync: boolean
|
||||
/** Method visibility */
|
||||
visibility: "public" | "private" | "protected"
|
||||
/** Whether it's static */
|
||||
isStatic: boolean
|
||||
}
|
||||
|
||||
export interface PropertyInfo {
|
||||
/** Property name */
|
||||
name: string
|
||||
/** Line number */
|
||||
line: number
|
||||
/** Property type (if available) */
|
||||
type?: string
|
||||
/** Property visibility */
|
||||
visibility: "public" | "private" | "protected"
|
||||
/** Whether it's static */
|
||||
isStatic: boolean
|
||||
/** Whether it's readonly */
|
||||
isReadonly: boolean
|
||||
}
|
||||
|
||||
export interface ClassInfo {
|
||||
/** Class name */
|
||||
name: string
|
||||
/** Start line number */
|
||||
lineStart: number
|
||||
/** End line number */
|
||||
lineEnd: number
|
||||
/** Class methods */
|
||||
methods: MethodInfo[]
|
||||
/** Class properties */
|
||||
properties: PropertyInfo[]
|
||||
/** Extended class name */
|
||||
extends?: string
|
||||
/** Implemented interfaces */
|
||||
implements: string[]
|
||||
/** Whether class is exported */
|
||||
isExported: boolean
|
||||
/** Whether class is abstract */
|
||||
isAbstract: boolean
|
||||
}
|
||||
|
||||
export interface InterfaceInfo {
|
||||
/** Interface name */
|
||||
name: string
|
||||
/** Start line number */
|
||||
lineStart: number
|
||||
/** End line number */
|
||||
lineEnd: number
|
||||
/** Interface properties */
|
||||
properties: PropertyInfo[]
|
||||
/** Extended interfaces */
|
||||
extends: string[]
|
||||
/** Whether interface is exported */
|
||||
isExported: boolean
|
||||
}
|
||||
|
||||
export interface TypeAliasInfo {
|
||||
/** Type alias name */
|
||||
name: string
|
||||
/** Line number */
|
||||
line: number
|
||||
/** Whether it's exported */
|
||||
isExported: boolean
|
||||
}
|
||||
|
||||
export interface FileAST {
|
||||
/** Import statements */
|
||||
imports: ImportInfo[]
|
||||
/** Export statements */
|
||||
exports: ExportInfo[]
|
||||
/** Function declarations */
|
||||
functions: FunctionInfo[]
|
||||
/** Class declarations */
|
||||
classes: ClassInfo[]
|
||||
/** Interface declarations */
|
||||
interfaces: InterfaceInfo[]
|
||||
/** Type alias declarations */
|
||||
typeAliases: TypeAliasInfo[]
|
||||
/** Whether parsing encountered errors */
|
||||
parseError: boolean
|
||||
/** Parse error message if any */
|
||||
parseErrorMessage?: string
|
||||
}
|
||||
|
||||
export function createEmptyFileAST(): FileAST {
|
||||
return {
|
||||
imports: [],
|
||||
exports: [],
|
||||
functions: [],
|
||||
classes: [],
|
||||
interfaces: [],
|
||||
typeAliases: [],
|
||||
parseError: false,
|
||||
}
|
||||
}
|
||||
26
packages/ipuaro/src/domain/value-objects/FileData.ts
Normal file
26
packages/ipuaro/src/domain/value-objects/FileData.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Represents file content with metadata for change detection.
|
||||
*/
|
||||
export interface FileData {
|
||||
/** File content split into lines */
|
||||
lines: string[]
|
||||
/** MD5 hash for change detection */
|
||||
hash: string
|
||||
/** File size in bytes */
|
||||
size: number
|
||||
/** Last modification timestamp (ms) */
|
||||
lastModified: number
|
||||
}
|
||||
|
||||
export function createFileData(
|
||||
lines: string[],
|
||||
hash: string,
|
||||
size: number,
|
||||
lastModified: number,
|
||||
): FileData {
|
||||
return { lines, hash, size, lastModified }
|
||||
}
|
||||
|
||||
export function isFileDataEqual(a: FileData, b: FileData): boolean {
|
||||
return a.hash === b.hash
|
||||
}
|
||||
50
packages/ipuaro/src/domain/value-objects/FileMeta.ts
Normal file
50
packages/ipuaro/src/domain/value-objects/FileMeta.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Represents computed metadata about a file.
|
||||
*/
|
||||
|
||||
export interface ComplexityMetrics {
|
||||
/** Lines of code (excluding empty and comments) */
|
||||
loc: number
|
||||
/** Maximum nesting depth */
|
||||
nesting: number
|
||||
/** Cyclomatic complexity score */
|
||||
cyclomaticComplexity: number
|
||||
/** Overall complexity score (0-100) */
|
||||
score: number
|
||||
}
|
||||
|
||||
export interface FileMeta {
|
||||
/** Complexity metrics for the file */
|
||||
complexity: ComplexityMetrics
|
||||
/** Files that this file imports (internal paths) */
|
||||
dependencies: string[]
|
||||
/** Files that import this file */
|
||||
dependents: string[]
|
||||
/** Whether file is a dependency hub (>5 dependents) */
|
||||
isHub: boolean
|
||||
/** Whether file is an entry point (index.ts or 0 dependents) */
|
||||
isEntryPoint: boolean
|
||||
/** File type classification */
|
||||
fileType: "source" | "test" | "config" | "types" | "unknown"
|
||||
}
|
||||
|
||||
export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
||||
return {
|
||||
complexity: {
|
||||
loc: 0,
|
||||
nesting: 0,
|
||||
cyclomaticComplexity: 1,
|
||||
score: 0,
|
||||
},
|
||||
dependencies: [],
|
||||
dependents: [],
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "unknown",
|
||||
...partial,
|
||||
}
|
||||
}
|
||||
|
||||
export function isHubFile(dependentCount: number): boolean {
|
||||
return dependentCount > 5
|
||||
}
|
||||
27
packages/ipuaro/src/domain/value-objects/ToolCall.ts
Normal file
27
packages/ipuaro/src/domain/value-objects/ToolCall.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Represents a tool call from the LLM.
|
||||
*/
|
||||
|
||||
export interface ToolCall {
|
||||
/** Unique identifier for this call */
|
||||
id: string
|
||||
/** Tool name */
|
||||
name: string
|
||||
/** Tool parameters */
|
||||
params: Record<string, unknown>
|
||||
/** Timestamp when call was made */
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export function createToolCall(
|
||||
id: string,
|
||||
name: string,
|
||||
params: Record<string, unknown>,
|
||||
): ToolCall {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
params,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
42
packages/ipuaro/src/domain/value-objects/ToolResult.ts
Normal file
42
packages/ipuaro/src/domain/value-objects/ToolResult.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Represents the result of a tool execution.
|
||||
*/
|
||||
|
||||
export interface ToolResult {
|
||||
/** Tool call ID this result belongs to */
|
||||
callId: string
|
||||
/** Whether execution was successful */
|
||||
success: boolean
|
||||
/** Result data (varies by tool) */
|
||||
data?: unknown
|
||||
/** Error message if failed */
|
||||
error?: string
|
||||
/** Execution time in milliseconds */
|
||||
executionTimeMs: number
|
||||
}
|
||||
|
||||
export function createSuccessResult(
|
||||
callId: string,
|
||||
data: unknown,
|
||||
executionTimeMs: number,
|
||||
): ToolResult {
|
||||
return {
|
||||
callId,
|
||||
success: true,
|
||||
data,
|
||||
executionTimeMs,
|
||||
}
|
||||
}
|
||||
|
||||
export function createErrorResult(
|
||||
callId: string,
|
||||
error: string,
|
||||
executionTimeMs: number,
|
||||
): ToolResult {
|
||||
return {
|
||||
callId,
|
||||
success: false,
|
||||
error,
|
||||
executionTimeMs,
|
||||
}
|
||||
}
|
||||
50
packages/ipuaro/src/domain/value-objects/UndoEntry.ts
Normal file
50
packages/ipuaro/src/domain/value-objects/UndoEntry.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Represents an undo entry for file changes.
|
||||
*/
|
||||
|
||||
export interface UndoEntry {
|
||||
/** Unique identifier */
|
||||
id: string
|
||||
/** Timestamp when change was made */
|
||||
timestamp: number
|
||||
/** File path that was modified */
|
||||
filePath: string
|
||||
/** Content before the change */
|
||||
previousContent: string[]
|
||||
/** Content after the change */
|
||||
newContent: string[]
|
||||
/** Human-readable description of the change */
|
||||
description: string
|
||||
/** Tool call ID that made this change */
|
||||
toolCallId?: string
|
||||
}
|
||||
|
||||
export function createUndoEntry(
|
||||
id: string,
|
||||
filePath: string,
|
||||
previousContent: string[],
|
||||
newContent: string[],
|
||||
description: string,
|
||||
toolCallId?: string,
|
||||
): UndoEntry {
|
||||
return {
|
||||
id,
|
||||
timestamp: Date.now(),
|
||||
filePath,
|
||||
previousContent,
|
||||
newContent,
|
||||
description,
|
||||
toolCallId,
|
||||
}
|
||||
}
|
||||
|
||||
export function canUndo(entry: UndoEntry, currentContent: string[]): boolean {
|
||||
return arraysEqual(entry.newContent, currentContent)
|
||||
}
|
||||
|
||||
function arraysEqual(a: string[], b: string[]): boolean {
|
||||
if (a.length !== b.length) {
|
||||
return false
|
||||
}
|
||||
return a.every((line, i) => line === b[i])
|
||||
}
|
||||
8
packages/ipuaro/src/domain/value-objects/index.ts
Normal file
8
packages/ipuaro/src/domain/value-objects/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Domain Value Objects
|
||||
export * from "./FileData.js"
|
||||
export * from "./FileAST.js"
|
||||
export * from "./FileMeta.js"
|
||||
export * from "./ChatMessage.js"
|
||||
export * from "./ToolCall.js"
|
||||
export * from "./ToolResult.js"
|
||||
export * from "./UndoEntry.js"
|
||||
17
packages/ipuaro/src/index.ts
Normal file
17
packages/ipuaro/src/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @puaros/ipuaro - Local AI agent for codebase operations
|
||||
*
|
||||
* Main entry point for the library.
|
||||
*/
|
||||
|
||||
// Domain exports
|
||||
export * from "./domain/index.js"
|
||||
|
||||
// Application exports
|
||||
export * from "./application/index.js"
|
||||
|
||||
// Shared exports
|
||||
export * from "./shared/index.js"
|
||||
|
||||
// Version
|
||||
export const VERSION = "0.1.0"
|
||||
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