Compare commits

...
6 Commits
Author SHA1 Message Date
Mervin Praison c86f660f66 Release v2.6.3 2025-12-28 07:31:42 +00:00
Mervin Praison a2d181dd74 Release v2.6.2 2025-12-27 07:58:18 +00:00
Mervin Praison 7411fdbb52 Release v2.6.1 2025-12-26 19:41:45 +00:00
Mervin Praison e7a8ce8eb7 Release v2.6.0 2025-12-26 19:31:15 +00:00
Mervin Praison 61fb094306 Release v1.2.3 - add comprehensive CLI feature exports and command definitions
- Bump praisonai-ts version from 1.2.2 to 1.2.3
- Add 18 new command definitions to CLI spec: query-rewrite, prompt-expand, router, context, handoff, vector, observability, voice, reranker, graph-rag, cache, db, scheduler, jobs, checkpoints, fast-context, autonomy, sandbox, repo-map, git, n8n, external-agents, flow, cost, interactive
- Export FileMemory and AutoMemory classes with factory functions and types from memory module
- Export
2025-12-26 18:41:37 +00:00
Mervin Praison 018e8c78ab refactor(ts): update CLI to v1.0.0 spec and change default model to gpt-4o-mini
- Change default agent model from gpt-5-nano to gpt-4o-mini in simple.ts
- Refactor CLI to implement CLI Spec v1.0.0 with lazy command loading
- Add parseArgs() function for spec-compliant argument parsing
- Add executeCommand() for dynamic command module loading
- Add comprehensive command definitions: memory, session, knowledge, skills, mcp, auto, image, research, guardrail, telemetry, planning
- Export CLI_SPEC_VERSION,
2025-12-26 14:07:05 +00:00
142 changed files with 23840 additions and 528 deletions

No files matched your search

+12 -8
View File
@@ -1994,19 +1994,23 @@ manager.delete_checkpoint("deploy-v1")
### Tools CLI:
```bash
# List all available tools
praisonai tools list
# Get info about a specific tool
praisonai tools info internet_search
# Search for tools
praisonai tools search "web"
# Show all commands
praisonai tools help
praisonai tools doctor
praisonai tools resolve shell_tool
praisonai tools discover
praisonai tools show-sources
praisonai tools show-sources --template ai-video-editor
```
| Command | Example | Docs |
|---------|---------|------|
| `tools list` | [example](examples/tools/) | [docs](https://docs.praison.ai/docs/cli/tools) |
| `tools resolve` | [example](examples/tools/example_tools_resolve.py) | [docs](https://docs.praison.ai/docs/cli/tools-resolve) |
| `tools discover` | [example](examples/tools/example_tools_discover.py) | [docs](https://docs.praison.ai/docs/cli/tools-discover) |
| `tools show-sources` | [example](examples/tools/example_tools_sources.py) | [docs](https://docs.praison.ai/docs/cli/tools-show-sources) |
### Handoff CLI:
```bash
# Enable agent-to-agent task delegation
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.7" \
"praisonai>=2.6.3" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.7" \
"praisonai>=2.6.3" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.7" \
"praisonai>=2.6.3" \
"praisonai[ui]" \
"praisonai[crewai]"
+53
View File
@@ -0,0 +1,53 @@
# Templates CLI
```bash
praisonai templates list
```
```bash
praisonai templates search video
```
```bash
praisonai templates info ai-video-editor
```
```bash
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4
```
```bash
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4 --offline
```
```bash
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4 --strict-tools
```
```bash
praisonai templates run ai-video-editor --input video.mp4 --tools ./my_tools.py
```
```bash
praisonai templates run ai-video-editor --input video.mp4 --tools-dir ./tools
```
```bash
praisonai templates run ai-video-editor --input video.mp4 --tools-source praisonai_tools.video
```
```bash
praisonai templates run ai-video-editor --input video.mp4 --no-template-tools-py
```
```bash
praisonai templates init my-project --template ai-video-editor
```
```bash
praisonai templates cache clear
```
```bash
praisonai templates cache list
```
+104
View File
@@ -0,0 +1,104 @@
# Templates Module
## Loading Templates
```python
from praisonai.templates.loader import TemplateLoader
loader = TemplateLoader()
# Load from local path
template = loader.load("./my-template")
# Load from URI
template = loader.load("ai-video-editor")
# Load offline
template = loader.load("ai-video-editor", offline=True)
# Check requirements
missing = loader.check_requirements(template)
```
## Template Discovery
```python
from praisonai.templates.discovery import TemplateDiscovery
discovery = TemplateDiscovery(
include_package=True,
include_defaults=True,
)
# Find template
result = discovery.find_template("ai-video-editor")
# List all templates
templates = discovery.list_templates()
```
## Template Config
```python
from praisonai.templates.loader import TemplateConfig
# Access template properties
template.name
template.description
template.version
template.author
template.requires
template.workflow_file
template.agents_file
template.path
```
## Tool Override Integration
```python
from praisonai.templates.tool_override import (
create_tool_registry_with_overrides,
resolve_tools,
)
# Build registry with template sources
tools_sources = template.requires.get("tools_sources", [])
registry = create_tool_registry_with_overrides(
tools_sources=tools_sources,
template_dir=str(template.path),
include_defaults=True,
)
# Resolve tool names
tools = resolve_tools(["shell_tool"], registry=registry)
```
## TEMPLATE.yaml Schema
```yaml
name: my-template
version: "1.0.0"
description: Template description
requires:
tools: [shell_tool]
packages: [praisonai-tools]
env: [OPENAI_API_KEY]
tools_sources:
- praisonai_tools.video
- ./local_tools.py
workflow: workflow.yaml
agents: agents.yaml
config:
input:
type: string
required: true
output:
type: string
defaults:
preset: podcast
```
+65
View File
@@ -0,0 +1,65 @@
# Tools CLI
```bash
praisonai tools list
```
```bash
praisonai tools info shell_tool
```
```bash
praisonai tools info internet_search
```
```bash
praisonai tools search "web"
```
```bash
praisonai tools search "file"
```
```bash
praisonai tools doctor
```
```bash
praisonai tools doctor --json
```
```bash
praisonai tools resolve shell_tool
```
```bash
praisonai tools resolve internet_search --template ai-video-editor
```
```bash
praisonai tools resolve my_tool --tools ./my_tools.py
```
```bash
praisonai tools resolve my_tool --tools-dir ./tools
```
```bash
praisonai tools discover
```
```bash
praisonai tools discover --include praisonai_tools.video
```
```bash
praisonai tools discover --entrypoints
```
```bash
praisonai tools show-sources
```
```bash
praisonai tools show-sources --template ai-video-editor
```
+90
View File
@@ -0,0 +1,90 @@
# Tools Module
## Tool Registry
```python
from praisonai.templates.tool_override import (
create_tool_registry_with_overrides,
resolve_tools,
)
# Create registry with defaults
registry = create_tool_registry_with_overrides(include_defaults=True)
# Create registry with overrides
registry = create_tool_registry_with_overrides(
override_files=["my_tools.py"],
override_dirs=["./tools"],
include_defaults=True,
)
# Create registry with tools_sources
registry = create_tool_registry_with_overrides(
tools_sources=["praisonai_tools.video"],
template_dir="./my-template",
include_defaults=True,
)
# Resolve tool names to callables
tools = resolve_tools(
["shell_tool", "internet_search"],
registry=registry,
)
```
## Tool Override Loader
```python
from praisonai.templates.tool_override import ToolOverrideLoader
loader = ToolOverrideLoader()
# Load from file
tools = loader.load_from_file("my_tools.py")
# Load from directory
tools = loader.load_from_directory("./tools")
# Load from module
tools = loader.load_from_module("praisonai_tools.video")
# Get default tool directories
dirs = loader.get_default_tool_dirs()
```
## Resolution Order
1. CLI `--tools` files (highest priority)
2. CLI `--tools-dir` directories
3. Template `tools_sources` (from TEMPLATE.yaml)
4. Template-local `tools.py`
5. Default dirs (`~/.praison/tools`, `~/.config/praison/tools`)
6. Package discovery (`praisonai_tools`)
7. Built-in tools (lowest priority)
## Creating Custom Tools
```python
from praisonai_tools import tool
@tool
def my_custom_tool(query: str) -> str:
"""My custom tool description.
Args:
query: The search query
"""
return f"Result for: {query}"
```
## Template tools_sources
```yaml
# TEMPLATE.yaml
requires:
tools: [shell_tool]
tools_sources:
- praisonai_tools.video
- ./local_tools.py
- ./tools_dir/
```
+25
View File
@@ -0,0 +1,25 @@
"""Example: Discover tools from installed packages."""
# Discover from praisonaiagents built-in tools
try:
from praisonaiagents.tools import TOOL_MAPPINGS
print("Built-in tools from praisonaiagents:")
for name in list(TOOL_MAPPINGS.keys())[:10]:
print(f" - {name}")
print(f" ... and {len(TOOL_MAPPINGS) - 10} more")
except ImportError:
print("praisonaiagents not installed")
# Discover from praisonai_tools
try:
import praisonai_tools
print("\nTools from praisonai_tools:")
# Check for video module
try:
from praisonai_tools import video
print(" - praisonai_tools.video (video editing)")
except ImportError:
pass
except ImportError:
print("praisonai_tools not installed")
+18
View File
@@ -0,0 +1,18 @@
"""Example: Resolve tool names to callable tools."""
from praisonai.templates.tool_override import (
create_tool_registry_with_overrides,
resolve_tools,
)
# Create registry with defaults
registry = create_tool_registry_with_overrides(include_defaults=True)
print(f"Registry contains {len(registry)} tools")
# Resolve tool names
tool_names = ["shell_tool", "internet_search"]
resolved = resolve_tools(tool_names, registry=registry)
print(f"\nResolved {len(resolved)} tools:")
for tool in resolved:
print(f" - {getattr(tool, '__name__', type(tool).__name__)}")
+14
View File
@@ -0,0 +1,14 @@
"""Example: Configure tool sources for templates."""
from praisonai.templates.tool_override import create_tool_registry_with_overrides
# Create registry with tools_sources
registry = create_tool_registry_with_overrides(
tools_sources=["praisonai_tools.video"],
include_defaults=True,
)
print(f"Registry with tools_sources: {len(registry)} tools")
# List some tools
for name in list(registry.keys())[:10]:
print(f" - {name}")
@@ -61,6 +61,57 @@ class Agent:
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
@classmethod
def from_template(
cls,
uri: str,
config: Optional[Dict[str, Any]] = None,
offline: bool = False,
**kwargs
) -> 'Agent':
"""
Create an Agent from a template.
Args:
uri: Template URI (local path, package ref, or github ref)
Examples:
- "./my-template" (local path)
- "transcript-generator" (default recipes repo)
- "github:owner/repo/[email protected]" (GitHub with version)
- "package:agent_recipes/transcript-generator" (installed package)
config: Optional configuration overrides
offline: If True, only use cached templates (no network)
**kwargs: Additional Agent constructor arguments
Returns:
Configured Agent instance
Example:
```python
from praisonaiagents import Agent
# From default recipes repo
agent = Agent.from_template("transcript-generator")
result = agent.chat("Transcribe ./audio.mp3")
# With config overrides
agent = Agent.from_template(
"data-transformer",
config={"output_format": "json"},
verbose=True
)
```
"""
try:
# Lazy import to avoid circular dependencies and keep core SDK lean
from praisonai.templates.loader import create_agent_from_template
return create_agent_from_template(uri, config=config, offline=offline, **kwargs)
except ImportError:
raise ImportError(
"Template support requires the 'praisonai' package. "
"Install with: pip install praisonai"
)
def _generate_tool_definition(self, function_name):
"""
Generate a tool definition from a function name by inspecting the function.
@@ -1694,7 +1745,11 @@ Your Goal: {self.goal}"""
# Special handling for MCP tools
# Check if tools is an MCP instance with the requested function name
from ..mcp.mcp import MCP
MCP = None
try:
from ..mcp.mcp import MCP
except ImportError:
pass # MCP not available
# Helper function to execute MCP tool
def _execute_mcp_tool(mcp_instance, func_name, args):
@@ -1729,7 +1784,7 @@ Your Goal: {self.goal}"""
return False, None
# Check if tools is a single MCP instance
if isinstance(self.tools, MCP):
if MCP is not None and isinstance(self.tools, MCP):
logging.debug(f"Looking for MCP tool {function_name}")
found, result = _execute_mcp_tool(self.tools, function_name, arguments)
if found:
@@ -1738,7 +1793,7 @@ Your Goal: {self.goal}"""
# Check if tools is a list that may contain MCP instances
if isinstance(self.tools, (list, tuple)):
for tool in self.tools:
if isinstance(tool, MCP):
if MCP is not None and isinstance(tool, MCP):
logging.debug(f"Looking for MCP tool {function_name} in MCP instance")
found, result = _execute_mcp_tool(tool, function_name, arguments)
if found:
@@ -2160,8 +2215,12 @@ Your Goal: {self.goal}"""
# Convert MCP tool objects to OpenAI format if needed
if tool_param is not None:
from ..mcp.mcp import MCP
if isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
MCP = None
try:
from ..mcp.mcp import MCP
except ImportError:
pass
if MCP is not None and isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
# Single MCP instance
logging.debug("Converting single MCP tool to OpenAI format")
openai_tool = tool_param.to_openai_tool()
@@ -2176,7 +2235,7 @@ Your Goal: {self.goal}"""
# List that may contain MCP instances - convert each MCP to OpenAI format
converted_tools = []
for t in tool_param:
if isinstance(t, MCP) and hasattr(t, 'to_openai_tool'):
if MCP is not None and isinstance(t, MCP) and hasattr(t, 'to_openai_tool'):
logging.debug("Converting MCP instance in list to OpenAI format")
openai_tools = t.to_openai_tool()
if isinstance(openai_tools, list):
@@ -3111,8 +3170,12 @@ Write the complete compiled report:"""
# Convert MCP tools if needed
if tool_param is not None:
from ..mcp.mcp import MCP
if isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
MCP = None
try:
from ..mcp.mcp import MCP
except ImportError:
pass
if MCP is not None and isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
openai_tool = tool_param.to_openai_tool()
if openai_tool:
if isinstance(openai_tool, list):
+219 -2
View File
@@ -104,6 +104,24 @@ class LLM:
OLLAMA_TOOL_USAGE_PROMPT = "Please analyze the request and use the available tools to help answer the question. Start by identifying what information you need."
OLLAMA_FINAL_ANSWER_PROMPT = "Based on the tool results above, please provide the final answer to the original question."
# Force tool usage prompt template (used when model ignores tools)
FORCE_TOOL_USAGE_PROMPT = """You MUST use one of the available tools to answer this question.
Do NOT provide a direct answer. Respond ONLY with a tool call.
Available tools: {tool_names}
Respond with a JSON object in this format:
{{"name": "tool_name", "arguments": {{"arg1": value1, "arg2": value2}}}}"""
# Tool call repair prompt template (used when tool call is malformed)
TOOL_CALL_REPAIR_PROMPT = """Your previous tool call was malformed: {error}
Please try again. Available tools:
{tool_schemas}
Respond with ONLY a valid JSON tool call in this format:
{{"name": "tool_name", "arguments": {{"arg1": value1, "arg2": value2}}}}"""
# Ollama iteration threshold for summary generation
OLLAMA_SUMMARY_ITERATION_THRESHOLD = 1
@@ -294,6 +312,16 @@ class LLM:
# Auto-detect XML tool format for known models, or allow manual override
self.xml_tool_format = extra_settings.get('xml_tool_format', 'auto')
# Tool calling reliability settings (for weak models like Ollama)
# These are auto-configured for Ollama providers if not explicitly set
self._max_tool_repairs_explicit = 'max_tool_repairs' in extra_settings
self._force_tool_usage_explicit = 'force_tool_usage' in extra_settings
self.max_tool_repairs = extra_settings.get('max_tool_repairs', 0) # Will be set to 2 for Ollama if not explicit
self.force_tool_usage = extra_settings.get('force_tool_usage', 'never') # Will be set to 'auto' for Ollama if not explicit
# Apply Ollama-specific defaults after model is set
self._apply_ollama_defaults()
# Token tracking
self.last_token_metrics: Optional[TokenMetrics] = None
self.session_token_metrics: Optional[TokenMetrics] = None
@@ -347,6 +375,16 @@ class LLM:
self._console = Console()
return self._console
def _apply_ollama_defaults(self):
"""Apply Ollama-specific defaults for tool calling reliability."""
if self._is_ollama_provider():
# Apply defaults only if not explicitly set by user
if not self._max_tool_repairs_explicit:
self.max_tool_repairs = 2
if not self._force_tool_usage_explicit:
self.force_tool_usage = 'auto'
logging.debug(f"[OLLAMA_RELIABILITY] Applied Ollama defaults: max_tool_repairs={self.max_tool_repairs}, force_tool_usage={self.force_tool_usage}")
def _is_ollama_provider(self) -> bool:
"""Detect if this is an Ollama provider regardless of naming convention"""
if not self.model:
@@ -562,14 +600,150 @@ class LLM:
def _format_ollama_tool_result_message(self, function_name: str, tool_result: Any) -> Dict[str, str]:
"""
Format tool result message for Ollama provider.
Simplified approach without hardcoded regex extraction.
Enhanced to instruct model to use the result for final answer.
"""
tool_result_str = str(tool_result)
return {
"role": "user",
"content": f"The {function_name} function returned: {tool_result_str}"
"content": f"""Tool execution complete.
Function: {function_name}
Result: {tool_result_str}
Now provide your final answer using this result."""
}
def _get_tool_names_for_prompt(self, formatted_tools: Optional[List]) -> str:
"""Extract tool names from formatted tools for prompts."""
if not formatted_tools:
return "None"
names = []
for tool in formatted_tools:
if isinstance(tool, dict) and 'function' in tool:
names.append(tool['function'].get('name', 'unknown'))
return ', '.join(names) if names else "None"
def _get_tool_schemas_for_prompt(self, formatted_tools: Optional[List]) -> str:
"""Generate compact tool schemas for repair prompts."""
if not formatted_tools:
return "None"
schemas = []
for tool in formatted_tools:
if isinstance(tool, dict) and 'function' in tool:
func = tool['function']
name = func.get('name', 'unknown')
params = func.get('parameters', {})
required = params.get('required', [])
props = params.get('properties', {})
param_strs = []
for pname, pinfo in props.items():
ptype = pinfo.get('type', 'any')
req = '*' if pname in required else ''
param_strs.append(f"{pname}{req}: {ptype}")
schemas.append(f"- {name}({', '.join(param_strs)})")
return '\n'.join(schemas) if schemas else "None"
def _should_force_tool_usage(self, response_text: str, tool_calls: Optional[List], formatted_tools: Optional[List], iteration_count: int) -> bool:
"""
Determine if we should force tool usage based on settings and context.
Returns True if:
- force_tool_usage == 'always' and tools exist and no tool_calls
- force_tool_usage == 'auto' and is Ollama and first iteration and tools exist and no tool_calls
"""
if not formatted_tools:
return False
if tool_calls:
return False
if self.force_tool_usage == 'never':
return False
elif self.force_tool_usage == 'always':
return True
elif self.force_tool_usage == 'auto':
# Auto mode: only for Ollama on first iteration when model ignores tools
return self._is_ollama_provider() and iteration_count == 0
return False
def _try_parse_tool_call_json(self, response_text: str, iteration_count: int) -> tuple:
"""
Try to parse tool call from JSON response text.
Returns:
tuple: (tool_calls list or None, parse_error string or None)
"""
if not response_text or not response_text.strip():
return None, None
try:
response_json = json.loads(response_text.strip())
if isinstance(response_json, dict) and "name" in response_json:
tool_calls = [{
"id": f"tool_{iteration_count}",
"type": "function",
"function": {
"name": response_json["name"],
"arguments": json.dumps(response_json.get("arguments", {}))
}
}]
return tool_calls, None
elif isinstance(response_json, list):
tool_calls = []
for idx, tool_json in enumerate(response_json):
if isinstance(tool_json, dict) and "name" in tool_json:
tool_calls.append({
"id": f"tool_{iteration_count}_{idx}",
"type": "function",
"function": {
"name": tool_json["name"],
"arguments": json.dumps(tool_json.get("arguments", {}))
}
})
return tool_calls if tool_calls else None, None
else:
return None, "Response is not a valid tool call format (missing 'name' field)"
except json.JSONDecodeError as e:
return None, f"JSON parse error: {str(e)}"
def _validate_tool_call(self, tool_call: Dict, formatted_tools: Optional[List]) -> Optional[str]:
"""
Validate a tool call against available tools.
Returns:
Error message string if invalid, None if valid
"""
if not formatted_tools:
return None
try:
func_info = tool_call.get('function', {})
tool_name = func_info.get('name', '')
arguments_str = func_info.get('arguments', '{}')
# Check if tool name exists
available_names = [t['function']['name'] for t in formatted_tools if isinstance(t, dict) and 'function' in t]
if tool_name not in available_names:
return f"Unknown tool '{tool_name}'. Available: {', '.join(available_names)}"
# Try to parse arguments
try:
arguments = json.loads(arguments_str) if isinstance(arguments_str, str) else arguments_str
except json.JSONDecodeError as e:
return f"Invalid arguments JSON: {str(e)}"
# Find the tool schema and validate required args
for tool in formatted_tools:
if isinstance(tool, dict) and tool.get('function', {}).get('name') == tool_name:
params = tool['function'].get('parameters', {})
required = params.get('required', [])
missing = [r for r in required if r not in arguments]
if missing:
return f"Missing required arguments for '{tool_name}': {', '.join(missing)}"
break
return None # Valid
except Exception as e:
return f"Validation error: {str(e)}"
def _process_stream_delta(self, delta, response_text: str, tool_calls: List[Dict], formatted_tools: Optional[List] = None) -> tuple:
"""
Process a streaming delta chunk to extract content and tool calls.
@@ -1637,6 +1811,18 @@ class LLM:
if tool_calls:
logging.debug(f"Parsed {len(tool_calls)} tool call(s) from XML format")
# Force tool usage logic: if model ignores tools but should use them
if self._should_force_tool_usage(response_text, tool_calls, formatted_tools, iteration_count):
tool_names = self._get_tool_names_for_prompt(formatted_tools)
force_prompt = self.FORCE_TOOL_USAGE_PROMPT.format(tool_names=tool_names)
logging.debug(f"[OLLAMA_RELIABILITY] Force tool usage triggered. Adding prompt for tools: {tool_names}")
messages.append({
"role": "user",
"content": force_prompt
})
iteration_count += 1
continue
# For Ollama, if response is empty but we have tools, prompt for tool usage
if self._is_ollama_provider() and (not response_text or response_text.strip() == "") and formatted_tools and iteration_count == 0:
messages.append({
@@ -1646,6 +1832,37 @@ class LLM:
iteration_count += 1
continue
# Tool call repair logic: validate and repair malformed tool calls
repair_attempt_count = getattr(self, '_current_repair_count', 0)
if tool_calls and self.max_tool_repairs > 0 and repair_attempt_count < self.max_tool_repairs:
# Validate each tool call
validation_errors = []
for tc in tool_calls:
error = self._validate_tool_call(tc, formatted_tools)
if error:
validation_errors.append(error)
if validation_errors:
# Tool call is invalid, attempt repair
error_msg = "; ".join(validation_errors)
tool_schemas = self._get_tool_schemas_for_prompt(formatted_tools)
repair_prompt = self.TOOL_CALL_REPAIR_PROMPT.format(
error=error_msg,
tool_schemas=tool_schemas
)
logging.debug(f"[OLLAMA_RELIABILITY] Tool call repair attempt {repair_attempt_count + 1}/{self.max_tool_repairs}: {error_msg}")
messages.append({
"role": "user",
"content": repair_prompt
})
self._current_repair_count = repair_attempt_count + 1
iteration_count += 1
tool_calls = None # Clear invalid tool calls
continue
# Reset repair count on successful tool call
self._current_repair_count = 0
# Handle tool calls - Sequential tool calling logic
if tool_calls and execute_tool_fn:
# Convert tool_calls to a serializable format for all providers
@@ -313,6 +313,56 @@ class Workflow:
"planning_llm": self.planning_llm
}
@classmethod
def from_template(
cls,
uri: str,
config: Optional[Dict[str, Any]] = None,
offline: bool = False,
**kwargs
) -> 'Workflow':
"""
Create a Workflow from a template.
Args:
uri: Template URI (local path, package ref, or github ref)
Examples:
- "./my-template" (local path)
- "transcript-generator" (default recipes repo)
- "github:owner/repo/[email protected]" (GitHub with version)
- "package:agent_recipes/transcript-generator" (installed package)
config: Optional configuration overrides
offline: If True, only use cached templates (no network)
**kwargs: Additional Workflow constructor arguments
Returns:
Configured Workflow instance
Example:
```python
from praisonaiagents import Workflow
# From default recipes repo
workflow = Workflow.from_template("transcript-generator")
result = workflow.run("./audio.mp3")
# With config overrides
workflow = Workflow.from_template(
"data-transformer",
config={"output_format": "json"}
)
```
"""
try:
# Lazy import to avoid circular dependencies and keep core SDK lean
from praisonai.templates.loader import create_workflow_from_template
return create_workflow_from_template(uri, config=config, offline=offline, **kwargs)
except ImportError:
raise ImportError(
"Template support requires the 'praisonai' package. "
"Install with: pip install praisonai"
)
def run(
self,
input: str = "",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.3.4"
version = "0.4.1"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.3.4"
version = "0.4.1"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -0,0 +1,52 @@
/**
* Evaluation Example - Testing agent accuracy and performance
*
* Run: npx ts-node examples/features/evaluation.ts
*/
import { Agent, accuracyEval, performanceEval } from '../../src';
async function main() {
// Create an agent to evaluate
const agent = new Agent({
instructions: "You are a math tutor. Answer math questions accurately.",
llm: "gpt-4o-mini",
verbose: false
});
console.log("=== Accuracy Evaluation ===");
// Test accuracy with expected outputs
const testCases = [
{ input: "What is 2 + 2?", expected: "4" },
{ input: "What is 10 * 5?", expected: "50" },
{ input: "What is 100 / 4?", expected: "25" }
];
for (const test of testCases) {
const response = await agent.chat(test.input);
const passed = response.includes(test.expected);
console.log(` ${passed ? '✅' : '❌'} "${test.input}" → Expected: ${test.expected}`);
}
console.log("\n=== Performance Evaluation ===");
// Measure response time
const iterations = 3;
const times: number[] = [];
for (let i = 0; i < iterations; i++) {
const start = Date.now();
await agent.chat("What is 5 + 5?");
const duration = Date.now() - start;
times.push(duration);
console.log(` Run ${i + 1}: ${duration}ms`);
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`\n Average: ${avgTime.toFixed(0)}ms`);
console.log(` Min: ${Math.min(...times)}ms`);
console.log(` Max: ${Math.max(...times)}ms`);
}
main().catch(console.error);
@@ -0,0 +1,49 @@
/**
* Guardrails Example - Input/output validation for agents
*
* Run: npx ts-node examples/features/guardrails.ts
*/
import { Agent, LLMGuardrail } from '../../src';
async function main() {
// Create an LLM-based guardrail
const contentGuardrail = new LLMGuardrail({
name: "ContentSafety",
criteria: "Content must be professional, non-offensive, and factually accurate",
llm: "gpt-4o-mini",
threshold: 0.7,
verbose: true
});
// Create agent
const agent = new Agent({
instructions: "You are a helpful assistant that provides accurate information.",
verbose: true
});
// Example: Check content before responding
const userInput = "Tell me about artificial intelligence";
// Validate input using check()
const inputCheck = await contentGuardrail.check(userInput);
console.log("Input validation:", inputCheck);
if (inputCheck.status === 'passed') {
const response = await agent.chat(userInput);
// Validate output
const outputCheck = await contentGuardrail.check(response);
console.log("Output validation:", outputCheck);
if (outputCheck.status === 'passed') {
console.log("\n✅ Response passed guardrails");
} else {
console.log("\n❌ Response failed guardrails:", outputCheck.reasoning);
}
} else {
console.log("❌ Input failed guardrails:", inputCheck.reasoning);
}
}
main().catch(console.error);
@@ -0,0 +1,64 @@
/**
* Knowledge Base Example - RAG with agents
*
* Run: npx ts-node examples/features/knowledge.ts
*/
import { Agent, BaseKnowledgeBase, type Knowledge } from '../../src';
async function main() {
// Create knowledge base
const kb = new BaseKnowledgeBase();
// Add knowledge entries
kb.addKnowledge({
id: "overview",
type: "documentation",
content: `
PraisonAI is a TypeScript SDK for building AI agents.
It supports multiple LLM providers including OpenAI, Anthropic, and Google.
Key features include:
- Simple Agent API with instructions
- Multi-agent orchestration with Agents class
- Workflow-based execution
- Tool integration with auto-schema generation
- Session persistence with db() factory
`,
metadata: { source: "docs", topic: "overview" }
});
kb.addKnowledge({
id: "agent-usage",
type: "documentation",
content: `
The Agent class is the primary entry point.
Create an agent with: new Agent({ instructions: "..." })
Use agent.chat("message") to interact.
Tools can be plain functions passed in the tools array.
`,
metadata: { source: "docs", topic: "agent" }
});
// Search the knowledge base
console.log("=== Searching Knowledge Base ===");
const results = kb.searchKnowledge("agent");
console.log("Found", results.length, "relevant entries");
results.forEach((r: Knowledge, i: number) => {
console.log(`\n[${i + 1}] ID: ${r.id}`);
console.log(String(r.content).substring(0, 100) + "...");
});
// Create agent that uses knowledge
const knowledgeContext = results.map((r: Knowledge) => r.content).join('\n\n');
const agent = new Agent({
instructions: `You are a helpful assistant. Use the following knowledge to answer questions:
${knowledgeContext}`,
verbose: true
});
console.log("\n=== Agent Response ===");
await agent.chat("How do I create an agent in PraisonAI?");
}
main().catch(console.error);
@@ -0,0 +1,37 @@
/**
* Memory Example - Using memory for agent context
*
* Run: npx ts-node examples/features/memory.ts
*/
import { Agent, Memory } from '../../src';
async function main() {
// Create a memory instance
const memory = new Memory({
maxEntries: 100,
maxTokens: 50000
});
// Add some context to memory
await memory.add("User prefers concise answers", "system", { priority: "high" });
await memory.add("User is working on a TypeScript project", "system", { topic: "programming" });
// Search memory
const results = await memory.search("TypeScript");
console.log("Memory search results:", results);
// Get recent entries
const recent = memory.getRecent(5);
console.log("Recent entries:", recent);
// Create agent
const agent = new Agent({
instructions: "You are a helpful assistant. Use context from memory.",
verbose: true
});
await agent.chat("Help me with my project");
}
main().catch(console.error);
@@ -0,0 +1,31 @@
/**
* Observability Example - Tracing and monitoring agents
*
* Run: npx ts-node examples/features/observability.ts
*/
import { Agent, setObservabilityAdapter, ConsoleObservabilityAdapter } from '../../src';
async function main() {
// Set up console observability (logs all traces)
const adapter = new ConsoleObservabilityAdapter();
setObservabilityAdapter(adapter);
console.log("=== Observability Enabled ===");
console.log("All agent operations will be traced to console.\n");
// Create agent
const agent = new Agent({
instructions: "You are a helpful assistant.",
verbose: true
});
// Run some operations - they will be traced
console.log("=== Running Agent ===");
await agent.chat("Hello! Tell me a joke.");
console.log("\n=== Trace Complete ===");
console.log("Check console output above for trace information.");
}
main().catch(console.error);
@@ -0,0 +1,50 @@
/**
* Planning Example - Using planning mode for complex tasks
*
* Run: npx ts-node examples/features/planning.ts
*/
import { Agent, Plan, PlanStep, TodoList, TodoItem } from '../../src';
async function main() {
// Create a plan for a complex task
console.log("=== Creating a Plan ===");
const plan = new Plan({
name: "Build a Web App",
description: "Plan to build a simple web application"
});
// Add steps to the plan
plan.addStep(new PlanStep({ description: "Initialize project structure" }));
plan.addStep(new PlanStep({ description: "Build API endpoints" }));
plan.addStep(new PlanStep({ description: "Create UI components" }));
plan.addStep(new PlanStep({ description: "Write and run tests" }));
plan.addStep(new PlanStep({ description: "Deploy to production" }));
console.log("Plan created with", plan.steps.length, "steps");
// Create a todo list
console.log("\n=== Creating a Todo List ===");
const todos = new TodoList();
todos.add(new TodoItem({ content: "Research frameworks", priority: "high" }));
todos.add(new TodoItem({ content: "Design database schema", priority: "medium" }));
todos.add(new TodoItem({ content: "Write documentation", priority: "low" }));
console.log("Todo list has", todos.items.length, "items");
// Create agent with planning context
const agent = new Agent({
instructions: `You are a project planning assistant.
Current plan: ${plan.name}
Steps: ${plan.steps.map((s: PlanStep) => s.description).join(', ')}
Help the user execute this plan.`,
verbose: true
});
await agent.chat("What should I do first?");
}
main().catch(console.error);
@@ -0,0 +1,45 @@
/**
* LLM Providers Example - Using different LLM providers
*
* Run: npx ts-node examples/features/providers.ts
*/
import { Agent } from '../../src';
async function main() {
// OpenAI (default)
console.log("=== OpenAI Agent ===");
const openaiAgent = new Agent({
instructions: "You are a helpful assistant.",
llm: "gpt-4o-mini" // or just omit for default
});
await openaiAgent.chat("Hello from OpenAI!");
// Anthropic Claude
console.log("\n=== Anthropic Agent ===");
const anthropicAgent = new Agent({
instructions: "You are a helpful assistant.",
llm: "anthropic/claude-3-haiku-20240307"
});
// Uncomment if you have ANTHROPIC_API_KEY set
// await anthropicAgent.chat("Hello from Anthropic!");
// Google Gemini
console.log("\n=== Google Agent ===");
const googleAgent = new Agent({
instructions: "You are a helpful assistant.",
llm: "google/gemini-1.5-flash"
});
// Uncomment if you have GOOGLE_API_KEY set
// await googleAgent.chat("Hello from Google!");
// Model string formats
console.log("\n=== Model String Formats ===");
console.log("Supported formats:");
console.log(" - 'gpt-4o-mini' (defaults to OpenAI)");
console.log(" - 'openai/gpt-4o'");
console.log(" - 'anthropic/claude-3-sonnet-20240229'");
console.log(" - 'google/gemini-1.5-pro'");
}
main().catch(console.error);
@@ -0,0 +1,31 @@
/**
* Sessions Example - Conversation persistence with session IDs
*
* Run: npx ts-node examples/features/sessions.ts
*/
import { Agent, db } from '../../src';
async function main() {
// Create an agent with session persistence
const agent = new Agent({
instructions: "You are a helpful assistant that remembers our conversation.",
db: db("sqlite:./sessions.db"),
sessionId: "user-alice-123"
});
// First conversation
console.log("=== First Conversation ===");
await agent.chat("My name is Alice and I love programming.");
// Continue the conversation - agent remembers context
console.log("\n=== Continuing Conversation ===");
await agent.chat("What's my name and what do I love?");
// Get session info
console.log("\n=== Session Info ===");
console.log("Session ID:", agent.getSessionId());
console.log("Run ID:", agent.getRunId());
}
main().catch(console.error);
@@ -0,0 +1,51 @@
/**
* Skills Example - Using agent skills
*
* Run: npx ts-node examples/features/skills.ts
*/
import { Agent, SkillManager, type Skill } from '../../src';
async function main() {
// Create a skill manager
const skillManager = new SkillManager({
paths: ['./.praison/skills'],
includeProject: true,
includeUser: true
});
// Discover available skills
console.log("=== Discovering Skills ===");
const skills = await skillManager.discover();
if (skills.length === 0) {
console.log("No skills found. Create a skill in .praison/skills/");
console.log("\nExample SKILL.md:");
console.log(`---
name: code-review
description: Reviews code for best practices
---
# Code Review Skill
When reviewing code:
1. Check for security issues
2. Verify error handling
3. Suggest improvements
`);
} else {
skills.forEach((skill: Skill) => {
console.log(` - ${skill.metadata.name}: ${skill.metadata.description}`);
});
}
// Create agent (skills would be loaded automatically if configured)
const agent = new Agent({
instructions: "You are a helpful coding assistant.",
verbose: true
});
await agent.chat("Help me write clean code");
}
main().catch(console.error);
@@ -0,0 +1,35 @@
/**
* Multi-Agent Pattern - Orchestrate multiple agents
*
* Run: npx ts-node examples/patterns/multi-agent.ts
*/
import { Agent, Agents } from '../../src';
// Create specialized agents
const researcher = new Agent({
instructions: "You are a research specialist. Thoroughly research the given topic and provide detailed findings.",
name: "Researcher"
});
const writer = new Agent({
instructions: "You are a professional writer. Take the research provided and write a clear, engaging summary.",
name: "Writer"
});
// Orchestrate agents - they run sequentially, passing output to the next
const agents = new Agents([researcher, writer]);
// Alternative: use config object for more control
// const agents = new Agents({
// agents: [researcher, writer],
// process: 'sequential', // or 'parallel'
// verbose: true
// });
agents.start().then(results => {
console.log("\n=== Research Agent Output ===");
console.log(results[0]);
console.log("\n=== Writer Agent Output ===");
console.log(results[1]);
});
@@ -0,0 +1,21 @@
/**
* Streaming Pattern - Real-time response streaming
*
* Run: npx ts-node examples/patterns/streaming.ts
*/
import { Agent } from '../../src';
// Create an agent with streaming enabled
const agent = new Agent({
instructions: "You are a creative storyteller. Write engaging, detailed stories.",
name: "Storyteller",
stream: true, // Enable streaming (default is true)
verbose: true
});
// The response will stream to stdout as it's generated
console.log("=== Streaming Story ===\n");
agent.chat("Write a short story about a robot learning to paint.").then(() => {
console.log("\n\n=== Story Complete ===");
});
@@ -0,0 +1,41 @@
/**
* Workflow Pattern - Step-based pipeline with agent integration
*
* Run: npx ts-node examples/patterns/workflow.ts
*/
import { Agent, Workflow } from '../../src';
// Create agents for the workflow
const researcher = new Agent({
instructions: "You are a research specialist. Research the given topic thoroughly.",
name: "Researcher"
});
const analyzer = new Agent({
instructions: "You are a data analyst. Analyze the research and extract key insights.",
name: "Analyzer"
});
const writer = new Agent({
instructions: "You are a professional writer. Write a clear report based on the analysis.",
name: "Writer"
});
// Create a workflow with agent steps
const workflow = new Workflow("Research Pipeline")
.agent(researcher, "Research the topic")
.agent(analyzer, "Analyze the research findings")
.agent(writer, "Write a summary report");
// Run the workflow
workflow.run("Artificial Intelligence trends in 2025").then(result => {
console.log("\n=== Workflow Results ===");
console.log(`Steps completed: ${result.results.length}`);
console.log(`Final output:\n${result.output}`);
// Access individual step results
result.results.forEach((step, i) => {
console.log(`\nStep ${i + 1} (${step.stepName}): ${step.status} in ${step.duration}ms`);
});
});
@@ -0,0 +1,10 @@
/**
* Hello Agent - The simplest possible PraisonAI example (3 lines)
*
* Run: npx ts-node examples/quickstart/hello-agent.ts
*/
import { Agent } from '../../src';
const agent = new Agent({ instructions: "You are a helpful assistant" });
agent.chat("Hello! Tell me a fun fact about AI.").then(console.log);
@@ -0,0 +1,15 @@
/**
* Agent with Persistence - Auto-save messages to database (4 lines)
*
* Run: npx ts-node examples/quickstart/with-persistence.ts
*/
import { Agent, db } from '../../src';
const agent = new Agent({
instructions: "You are a helpful assistant with memory",
db: db("memory:"), // Use in-memory db for demo (use "sqlite:./data.db" for file)
sessionId: "my-session"
});
agent.chat("Remember that my favorite color is blue.").then(console.log);
@@ -0,0 +1,17 @@
/**
* Agent with Tools - Pass plain functions as tools (5 lines)
*
* Run: npx ts-node examples/quickstart/with-tools.ts
*/
import { Agent } from '../../src';
// Define a simple tool as a plain function
const getWeather = (city: string) => `Weather in ${city}: 22°C, Sunny`;
const agent = new Agent({
instructions: "You are a weather assistant. Use the getWeather tool to answer questions.",
tools: [getWeather]
});
agent.chat("What's the weather like in Paris?").then(console.log);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "praisonai",
"version": "1.2.2",
"version": "1.3.0",
"description": "PraisonAI TypeScript AI Agents Framework - Node.js, npm, and Javascript AI Agents Framework",
"main": "dist/index.js",
"types": "dist/index.d.ts",
+64
View File
@@ -0,0 +1,64 @@
{
"command": "chat",
"description": "Golden test fixtures for chat command JSON output parity",
"test_cases": [
{
"name": "simple_chat_success",
"input": {
"args": ["Hello, how are you?"],
"options": {
"output": "json"
}
},
"expected_schema": {
"success": true,
"data": {
"response": "string"
},
"meta": {
"duration_ms": "number",
"model": "string",
"tokens": {
"input": "number",
"output": "number"
}
}
}
},
{
"name": "chat_missing_prompt",
"input": {
"args": [],
"options": {
"output": "json"
}
},
"expected": {
"success": false,
"error": {
"code": "MISSING_ARGUMENT",
"message": "Please provide a prompt"
}
}
},
{
"name": "chat_with_model",
"input": {
"args": ["Test prompt"],
"options": {
"model": "openai/gpt-4o-mini",
"output": "json"
}
},
"expected_schema": {
"success": true,
"data": {
"response": "string"
},
"meta": {
"model": "openai/gpt-4o-mini"
}
}
}
]
}
+43
View File
@@ -0,0 +1,43 @@
{
"command": "run",
"description": "Golden test fixtures for run command JSON output parity",
"test_cases": [
{
"name": "run_success",
"input": {
"args": ["Analyze this data"],
"options": {
"output": "json"
}
},
"expected_schema": {
"success": true,
"data": {
"result": "string",
"agent": "string",
"task": "string"
},
"meta": {
"duration_ms": "number",
"model": "string"
}
}
},
{
"name": "run_missing_task",
"input": {
"args": [],
"options": {
"output": "json"
}
},
"expected": {
"success": false,
"error": {
"code": "MISSING_ARGUMENT",
"message": "Please provide a task"
}
}
}
]
}
@@ -0,0 +1,43 @@
{
"command": "workflow",
"description": "Golden test fixtures for workflow command JSON output parity",
"test_cases": [
{
"name": "workflow_success",
"input": {
"args": ["workflow.yaml"],
"options": {
"output": "json"
}
},
"expected_schema": {
"success": true,
"data": {
"workflow": "string",
"steps": "number",
"results": "object"
},
"meta": {
"duration_ms": "number",
"model": "string"
}
}
},
{
"name": "workflow_missing_file",
"input": {
"args": [],
"options": {
"output": "json"
}
},
"expected": {
"success": false,
"error": {
"code": "MISSING_ARGUMENT",
"message": "Please provide a workflow file path"
}
}
}
]
}
+26 -7
View File
@@ -1,12 +1,31 @@
// Export implementation based on mode
/**
* Agent Module - Core agent classes for PraisonAI
*
* The primary exports are:
* - Agent: Single agent with instructions, tools, and optional persistence
* - Agents: Multi-agent orchestration (alias for PraisonAIAgents)
* - Workflow: Step-based workflow execution (from workflows module)
*/
// Core exports - the main API surface
export { Agent, PraisonAIAgents, Agents } from './simple';
export type { SimpleAgentConfig, PraisonAIAgentsConfig } from './simple';
// Task support (for advanced use cases)
export { Task } from './types';
export type { TaskConfig, AgentConfig as TaskAgentConfig } from './types';
// Legacy compatibility - setTaskMode is deprecated but kept for backward compat
let useTaskMode = false;
/**
* @deprecated Task mode is no longer needed. Use Agent with role/goal/backstory instead.
*/
export function setTaskMode(enabled: boolean) {
if (enabled) {
console.warn(
'setTaskMode() is deprecated. Use Agent({ role, goal, backstory }) instead of task mode.'
);
}
useTaskMode = enabled;
}
export { Agent, PraisonAIAgents, Task } from './proxy';
export type { ProxyAgentConfig } from './proxy';
export type { AgentConfig } from './types';
export type { TaskConfig } from './types';
export type { PraisonAIAgentsConfig, SimpleAgentConfig } from './simple';
+191 -4
View File
@@ -1,17 +1,84 @@
import { OpenAIService } from '../llm/openai';
import { Logger } from '../utils/logger';
import type { ChatCompletionTool } from 'openai/resources/chat/completions';
import type { DbAdapter, DbMessage, DbRun } from '../db/types';
import { randomUUID } from 'crypto';
/**
* Agent Configuration
*
* The Agent class is the primary entry point for PraisonAI.
* It supports both simple instruction-based agents and advanced configurations.
*
* @example Simple usage (3 lines)
* ```typescript
* import { Agent } from 'praisonai';
* const agent = new Agent({ instructions: "You are helpful" });
* await agent.chat("Hello!");
* ```
*
* @example With tools (5 lines)
* ```typescript
* const getWeather = (city: string) => `Weather in ${city}: 20°C`;
* const agent = new Agent({
* instructions: "You provide weather info",
* tools: [getWeather]
* });
* await agent.chat("Weather in Paris?");
* ```
*
* @example With persistence (4 lines)
* ```typescript
* import { Agent, db } from 'praisonai';
* const agent = new Agent({
* instructions: "You are helpful",
* db: db("sqlite:./data.db"),
* sessionId: "my-session"
* });
* await agent.chat("Hello!");
* ```
*/
export interface SimpleAgentConfig {
/** Agent instructions/system prompt (required) */
instructions: string;
/** Agent name (auto-generated if not provided) */
name?: string;
/** Enable verbose logging (default: true) */
verbose?: boolean;
/** Enable pretty output formatting */
pretty?: boolean;
/**
* LLM model to use. Accepts:
* - Model name: "gpt-4o-mini", "claude-3-sonnet"
* - Provider/model: "openai/gpt-4o", "anthropic/claude-3"
* Default: "gpt-4o-mini"
*/
llm?: string;
/** Enable markdown formatting in responses */
markdown?: boolean;
/** Enable streaming responses (default: true) */
stream?: boolean;
/**
* Tools available to the agent.
* Can be plain functions (auto-schema) or OpenAI tool definitions.
*/
tools?: any[] | Function[];
/** Map of tool function implementations */
toolFunctions?: Record<string, Function>;
/** Database adapter for persistence */
db?: DbAdapter;
/** Session ID for conversation persistence */
sessionId?: string;
/** Run ID for tracing (auto-generated if not provided) */
runId?: string;
// Advanced mode (role/goal/backstory) - for compatibility
/** Agent role (advanced mode) */
role?: string;
/** Agent goal (advanced mode) */
goal?: string;
/** Agent backstory (advanced mode) */
backstory?: string;
}
export class Agent {
@@ -25,16 +92,36 @@ export class Agent {
private llmService: OpenAIService;
private tools?: any[];
private toolFunctions: Record<string, Function> = {};
private dbAdapter?: DbAdapter;
private sessionId: string;
private runId: string;
private messages: Array<{ role: string; content: string | null; tool_calls?: any[]; tool_call_id?: string }> = [];
constructor(config: SimpleAgentConfig) {
this.instructions = config.instructions;
// Build instructions from either simple or advanced mode
if (config.instructions) {
this.instructions = config.instructions;
} else if (config.role || config.goal || config.backstory) {
// Advanced mode: construct instructions from role/goal/backstory
const parts: string[] = [];
if (config.role) parts.push(`You are a ${config.role}.`);
if (config.goal) parts.push(`Your goal is: ${config.goal}`);
if (config.backstory) parts.push(`Background: ${config.backstory}`);
this.instructions = parts.join('\n');
} else {
this.instructions = 'You are a helpful AI assistant.';
}
this.name = config.name || `Agent_${Math.random().toString(36).substr(2, 9)}`;
this.verbose = config.verbose ?? process.env.PRAISON_VERBOSE !== 'false';
this.pretty = config.pretty ?? process.env.PRAISON_PRETTY === 'true';
this.llm = config.llm || 'gpt-5-nano';
this.llm = config.llm || process.env.OPENAI_MODEL_NAME || process.env.PRAISONAI_MODEL || 'gpt-4o-mini';
this.markdown = config.markdown ?? true;
this.stream = config.stream ?? true;
this.tools = config.tools;
this.dbAdapter = config.db;
this.sessionId = config.sessionId || randomUUID();
this.runId = config.runId || randomUUID();
this.llmService = new OpenAIService(this.llm);
// Configure logging
@@ -315,7 +402,19 @@ export class Agent {
}
async chat(prompt: string, previousResult?: string): Promise<string> {
return this.start(prompt, previousResult);
// Persist user message if db is configured
if (this.dbAdapter) {
await this.persistMessage('user', prompt);
}
const response = await this.start(prompt, previousResult);
// Persist assistant response if db is configured
if (this.dbAdapter) {
await this.persistMessage('assistant', response);
}
return response;
}
async execute(previousResult?: string): Promise<string> {
@@ -323,6 +422,41 @@ export class Agent {
return this.start(this.instructions, previousResult);
}
/**
* Persist a message to the database
*/
private async persistMessage(role: 'user' | 'assistant' | 'system' | 'tool', content: string): Promise<void> {
if (!this.dbAdapter) return;
try {
const message: DbMessage = {
id: randomUUID(),
sessionId: this.sessionId,
runId: this.runId,
role,
content,
createdAt: Date.now()
};
await this.dbAdapter.saveMessage(message);
} catch (error) {
await Logger.warn('Failed to persist message:', error);
}
}
/**
* Get the session ID for this agent
*/
getSessionId(): string {
return this.sessionId;
}
/**
* Get the run ID for this agent
*/
getRunId(): string {
return this.runId;
}
getResult(): string | null {
return null;
}
@@ -332,6 +466,9 @@ export class Agent {
}
}
/**
* Configuration for multi-agent orchestration
*/
export interface PraisonAIAgentsConfig {
agents: Agent[];
tasks?: string[];
@@ -340,6 +477,28 @@ export interface PraisonAIAgentsConfig {
process?: 'sequential' | 'parallel';
}
/**
* Multi-agent orchestration class
*
* @example Simple array syntax
* ```typescript
* import { Agent, Agents } from 'praisonai';
*
* const researcher = new Agent({ instructions: "Research the topic" });
* const writer = new Agent({ instructions: "Write based on research" });
*
* const agents = new Agents([researcher, writer]);
* await agents.start();
* ```
*
* @example Config object syntax
* ```typescript
* const agents = new Agents({
* agents: [researcher, writer],
* process: 'parallel'
* });
* ```
*/
export class PraisonAIAgents {
private agents: Agent[];
private tasks: string[];
@@ -347,7 +506,16 @@ export class PraisonAIAgents {
private pretty: boolean;
private process: 'sequential' | 'parallel';
constructor(config: PraisonAIAgentsConfig) {
/**
* Create a multi-agent orchestration
* @param configOrAgents - Either an array of agents or a config object
*/
constructor(configOrAgents: PraisonAIAgentsConfig | Agent[]) {
// Support array syntax: new Agents([a1, a2])
const config: PraisonAIAgentsConfig = Array.isArray(configOrAgents)
? { agents: configOrAgents }
: configOrAgents;
this.agents = config.agents;
this.verbose = config.verbose ?? process.env.PRAISON_VERBOSE !== 'false';
this.pretty = config.pretty ?? process.env.PRAISON_PRETTY === 'true';
@@ -425,3 +593,22 @@ export class PraisonAIAgents {
return this.start();
}
}
/**
* Agents - Alias for PraisonAIAgents
*
* This is the recommended class name for multi-agent orchestration.
* PraisonAIAgents is kept for backward compatibility.
*
* @example
* ```typescript
* import { Agent, Agents } from 'praisonai';
*
* const agents = new Agents([
* new Agent({ instructions: "Research the topic" }),
* new Agent({ instructions: "Write based on research" })
* ]);
* await agents.start();
* ```
*/
export const Agents = PraisonAIAgents;
+148
View File
@@ -0,0 +1,148 @@
/**
* Agent command - Run a single agent with instructions
*
* Usage:
* praisonai-ts agent chat "Hello, how are you?"
* praisonai-ts agent run --instructions "You are helpful" "Tell me a joke"
*/
import { Agent } from '../../agent';
import { resolveConfig } from '../config/resolve';
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES, normalizeError } from '../output/errors';
export interface AgentOptions {
instructions?: string;
model?: string;
stream?: boolean;
verbose?: boolean;
profile?: string;
config?: string;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
sessionId?: string;
}
/**
* Execute agent chat subcommand
*/
export async function executeChat(args: string[], options: AgentOptions): Promise<void> {
const prompt = args[0];
if (!prompt) {
if (options.json || options.output === 'json') {
printError(ERROR_CODES.MISSING_ARG, 'Please provide a prompt');
} else {
await pretty.error('Please provide a prompt');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const config = resolveConfig({
configPath: options.config,
profile: options.profile,
model: options.model,
verbose: options.verbose,
stream: options.stream
});
const startTime = Date.now();
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
const agent = new Agent({
instructions: options.instructions || 'You are a helpful AI assistant.',
llm: config.model,
stream: config.stream && outputFormat !== 'json',
verbose: config.verbose,
sessionId: options.sessionId
});
const response = await agent.chat(prompt);
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess(
{
response,
agent: agent.name,
sessionId: agent.getSessionId()
},
{
duration_ms: duration,
model: config.model
}
));
} else if (outputFormat === 'text') {
console.log(response);
} else {
// Pretty output - response already printed if streaming
if (!config.stream) {
console.log(response);
}
console.log(); // Newline
await pretty.dim(`Completed in ${duration}ms`);
}
} catch (error) {
const cliError = normalizeError(error);
if (outputFormat === 'json') {
outputJson(formatError(cliError.code, cliError.message, cliError.details));
} else {
await pretty.error(cliError.message);
if (config.verbose && error instanceof Error && error.stack) {
await pretty.dim(error.stack);
}
}
process.exit(cliError.exitCode);
}
}
/**
* Execute agent run subcommand (same as chat but requires instructions)
*/
export async function executeRun(args: string[], options: AgentOptions): Promise<void> {
if (!options.instructions) {
if (options.json || options.output === 'json') {
printError(ERROR_CODES.MISSING_ARG, 'Please provide --instructions');
} else {
await pretty.error('Please provide --instructions');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
return executeChat(args, options);
}
/**
* Main execute function - routes to subcommands
*/
export async function execute(args: string[], options: AgentOptions): Promise<void> {
const subcommand = args[0];
const subArgs = args.slice(1);
switch (subcommand) {
case 'chat':
return executeChat(subArgs, options);
case 'run':
return executeRun(subArgs, options);
default:
// If no subcommand, treat the first arg as a prompt for chat
if (subcommand) {
return executeChat(args, options);
}
if (options.json || options.output === 'json') {
printError(ERROR_CODES.MISSING_ARG, 'Please provide a subcommand (chat, run) or a prompt');
} else {
await pretty.error('Usage: praisonai-ts agent [chat|run] <prompt>');
await pretty.info(' agent chat "Hello" - Chat with default agent');
await pretty.info(' agent run --instructions "..." "Hello" - Run with custom instructions');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
}
+167
View File
@@ -0,0 +1,167 @@
/**
* Agents command - Run multi-agent orchestration
*
* Usage:
* praisonai-ts agents run --agents "researcher,writer" "Research AI trends"
*/
import { Agent, Agents } from '../../agent';
import { resolveConfig } from '../config/resolve';
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES, normalizeError } from '../output/errors';
export interface AgentsOptions {
agents?: string; // Comma-separated agent instructions
process?: 'sequential' | 'parallel';
model?: string;
verbose?: boolean;
profile?: string;
config?: string;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
/**
* Parse agent definitions from comma-separated string
*/
function parseAgentDefinitions(agentsStr: string): string[] {
return agentsStr.split(',').map(s => s.trim()).filter(s => s.length > 0);
}
/**
* Execute agents run subcommand
*/
export async function executeRun(args: string[], options: AgentsOptions): Promise<void> {
const prompt = args[0];
if (!prompt) {
if (options.json || options.output === 'json') {
printError(ERROR_CODES.MISSING_ARG, 'Please provide a prompt/task');
} else {
await pretty.error('Please provide a prompt/task');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const config = resolveConfig({
configPath: options.config,
profile: options.profile,
model: options.model,
verbose: options.verbose
});
const startTime = Date.now();
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
// Create agents from definitions or use defaults
let agentList: Agent[];
if (options.agents) {
const definitions = parseAgentDefinitions(options.agents);
agentList = definitions.map((instructions, i) =>
new Agent({
instructions,
name: `Agent_${i + 1}`,
llm: config.model,
verbose: config.verbose
})
);
} else {
// Default: create a researcher and writer
agentList = [
new Agent({
instructions: `Research the following topic thoroughly: ${prompt}`,
name: 'Researcher',
llm: config.model,
verbose: config.verbose
}),
new Agent({
instructions: 'Summarize and write a clear report based on the research provided.',
name: 'Writer',
llm: config.model,
verbose: config.verbose
})
];
}
if (outputFormat !== 'json') {
await pretty.heading(`Running ${agentList.length} agents (${options.process || 'sequential'})`);
}
const agents = new Agents({
agents: agentList,
process: options.process || 'sequential',
verbose: config.verbose
});
const results = await agents.start();
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess(
{
results,
agentCount: agentList.length,
process: options.process || 'sequential'
},
{
duration_ms: duration,
model: config.model
}
));
} else if (outputFormat === 'text') {
results.forEach((result, i) => {
console.log(`\n--- Agent ${i + 1} ---\n${result}`);
});
} else {
// Pretty output
console.log();
await pretty.success(`Completed ${agentList.length} agents in ${duration}ms`);
}
} catch (error) {
const cliError = normalizeError(error);
if (outputFormat === 'json') {
outputJson(formatError(cliError.code, cliError.message, cliError.details));
} else {
await pretty.error(cliError.message);
if (config.verbose && error instanceof Error && error.stack) {
await pretty.dim(error.stack);
}
}
process.exit(cliError.exitCode);
}
}
/**
* Main execute function - routes to subcommands
*/
export async function execute(args: string[], options: AgentsOptions): Promise<void> {
const subcommand = args[0];
const subArgs = args.slice(1);
switch (subcommand) {
case 'run':
return executeRun(subArgs, options);
default:
// If no subcommand, treat as run
if (subcommand) {
return executeRun(args, options);
}
if (options.json || options.output === 'json') {
printError(ERROR_CODES.MISSING_ARG, 'Please provide a task/prompt');
} else {
await pretty.error('Usage: praisonai-ts agents [run] <task>');
await pretty.info(' agents run "Research AI" - Run with default agents');
await pretty.info(' agents run --agents "..." "Research AI" - Run with custom agents');
await pretty.info(' agents run --process parallel "Research AI" - Run in parallel');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
}
+90
View File
@@ -0,0 +1,90 @@
/**
* Auto command - AutoAgents for automatic agent generation
*/
import { AutoAgents, createAutoAgents } from '../../auto';
import { resolveConfig } from '../config/resolve';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface AutoOptions {
model?: string;
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
pattern?: string;
agents?: number;
}
export async function execute(args: string[], options: AutoOptions): Promise<void> {
const topic = args.join(' ');
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
const config = resolveConfig(options);
if (!topic) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a topic for auto-generation'));
} else {
await pretty.error('Please provide a topic for auto-generation');
await pretty.dim('Usage: praisonai-ts auto "Create a research team"');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
try {
if (outputFormat !== 'json') {
await pretty.info(`Generating agents for: "${topic}"`);
}
const autoAgents = createAutoAgents({
llm: config.model,
pattern: options.pattern as any,
verbose: options.verbose
});
const result = await autoAgents.generate(topic);
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess({
topic,
agents: result.agents,
tasks: result.tasks,
pattern: result.pattern
}, {
duration_ms: duration,
model: config.model
}));
} else {
await pretty.heading('Generated Team');
await pretty.plain(`Pattern: ${result.pattern}`);
await pretty.newline();
await pretty.plain('Agents:');
for (const agent of result.agents) {
await pretty.plain(` • ${agent.name}: ${agent.role}`);
}
await pretty.newline();
await pretty.plain('Tasks:');
for (const task of result.tasks) {
await pretty.plain(` • ${task.description?.substring(0, 60)}...`);
}
await pretty.newline();
await pretty.success(`Generated in ${duration}ms`);
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
@@ -0,0 +1,163 @@
/**
* CLI command: autonomy
* Autonomy mode management for agent actions
*/
import { AutonomyManager, createAutonomyManager, MODE_POLICIES, type AutonomyMode } from '../features/autonomy-mode';
let manager: AutonomyManager | null = null;
function getManager(): AutonomyManager {
if (!manager) {
manager = createAutonomyManager();
}
return manager;
}
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
switch (subcommand) {
case 'status':
await handleStatus(isJson);
break;
case 'set':
await handleSet(args.slice(1), isJson);
break;
case 'policies':
await handlePolicies(args.slice(1), isJson);
break;
case 'reset':
await handleReset(isJson);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleStatus(isJson: boolean): Promise<void> {
const mgr = getManager();
const summary = mgr.getSummary();
const mode = mgr.getMode();
const actionCount = mgr.getActionCount();
if (isJson) {
console.log(JSON.stringify({
success: true,
mode,
actionCount,
policies: MODE_POLICIES[mode]
}, null, 2));
} else {
console.log(summary);
}
}
async function handleSet(args: string[], isJson: boolean): Promise<void> {
const mode = args[0] as AutonomyMode;
if (!mode || !['suggest', 'auto_edit', 'full_auto'].includes(mode)) {
console.error('Error: Valid mode required (suggest, auto_edit, full_auto)');
process.exit(1);
}
const mgr = getManager();
mgr.setMode(mode);
if (isJson) {
console.log(JSON.stringify({ success: true, mode }));
} else {
console.log(`✓ Autonomy mode set to: ${mode}`);
console.log('\nPolicies for this mode:');
for (const policy of MODE_POLICIES[mode]) {
const status = policy.requiresApproval ? '⚠️ requires approval' : '✓ auto-approved';
console.log(` ${policy.action}: ${status}`);
}
}
}
async function handlePolicies(args: string[], isJson: boolean): Promise<void> {
const mode = (args[0] as AutonomyMode) || 'suggest';
if (!['suggest', 'auto_edit', 'full_auto'].includes(mode)) {
console.error('Error: Valid mode required (suggest, auto_edit, full_auto)');
process.exit(1);
}
const policies = MODE_POLICIES[mode];
if (isJson) {
console.log(JSON.stringify({ success: true, mode, policies }, null, 2));
} else {
console.log(`Policies for mode: ${mode}\n`);
for (const policy of policies) {
const status = policy.requiresApproval ? '⚠️ requires approval' : '✓ auto-approved';
console.log(` ${policy.action.padEnd(20)} ${status}`);
}
}
}
async function handleReset(isJson: boolean): Promise<void> {
const mgr = getManager();
mgr.clearRemembered();
mgr.resetActionCount();
mgr.setMode('suggest');
if (isJson) {
console.log(JSON.stringify({ success: true, message: 'Autonomy state reset' }));
} else {
console.log('✓ Autonomy state reset');
console.log(' Mode: suggest');
console.log(' Remembered decisions: cleared');
console.log(' Action count: 0');
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'autonomy',
description: 'Autonomy mode management for agent actions',
subcommands: {
status: 'Show current autonomy status',
set: 'Set autonomy mode',
policies: 'Show policies for a mode',
reset: 'Reset autonomy state'
},
modes: {
suggest: 'Requires approval for most actions (default)',
auto_edit: 'Auto-approves file edits, requires approval for others',
full_auto: 'Auto-approves most actions (use with caution)'
},
flags: {
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts autonomy status',
'praisonai-ts autonomy set suggest',
'praisonai-ts autonomy set auto_edit',
'praisonai-ts autonomy set full_auto',
'praisonai-ts autonomy policies suggest',
'praisonai-ts autonomy reset'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Autonomy - Agent action approval management\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nModes:');
for (const [mode, desc] of Object.entries(help.modes)) {
console.log(` ${mode.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
+117
View File
@@ -0,0 +1,117 @@
/**
* Cache command - Caching management
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface CacheOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: CacheOptions): Promise<void> {
const action = args[0] || 'help';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'info':
await showInfo(outputFormat);
break;
case 'providers':
await listProviders(outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function showInfo(outputFormat: string): Promise<void> {
const info = {
feature: 'Cache',
description: 'Caching system for LLM responses and computations',
providers: [
{ name: 'MemoryCache', description: 'In-memory cache (default)' },
{ name: 'FileCache', description: 'File-based persistent cache' }
],
capabilities: [
'Cache LLM responses',
'Reduce API costs',
'Improve response times',
'TTL-based expiration',
'Configurable cache keys'
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(info));
} else {
await pretty.heading('Cache');
await pretty.plain(info.description);
await pretty.newline();
await pretty.plain('Providers:');
for (const p of info.providers) {
await pretty.plain(` • ${p.name}: ${p.description}`);
}
await pretty.newline();
await pretty.plain('Capabilities:');
for (const cap of info.capabilities) {
await pretty.plain(` • ${cap}`);
}
}
}
async function listProviders(outputFormat: string): Promise<void> {
const providers = [
{ name: 'memory', description: 'In-memory cache', available: true },
{ name: 'file', description: 'File-based cache', available: true }
];
if (outputFormat === 'json') {
outputJson(formatSuccess({ providers }));
} else {
await pretty.heading('Cache Providers');
for (const p of providers) {
const status = p.available ? '✓' : '✗';
await pretty.plain(` ${status} ${p.name.padEnd(15)} ${p.description}`);
}
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'cache',
description: 'Caching management for LLM responses',
subcommands: [
{ name: 'info', description: 'Show cache feature information' },
{ name: 'providers', description: 'List available cache providers' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Cache Command');
await pretty.plain(help.description);
await pretty.newline();
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
}
}
@@ -0,0 +1,252 @@
/**
* CLI command: checkpoints
* Session state checkpointing and recovery
*/
import { CheckpointManager, createCheckpointManager, createFileCheckpointStorage } from '../features/checkpoints';
let manager: CheckpointManager | null = null;
function getManager(storagePath?: string): CheckpointManager {
if (!manager) {
const storage = storagePath ? createFileCheckpointStorage(storagePath) : undefined;
manager = createCheckpointManager({ storage });
}
return manager;
}
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
const storagePath = options.storage as string | undefined;
switch (subcommand) {
case 'create':
await handleCreate(args.slice(1), options, isJson, storagePath);
break;
case 'list':
await handleList(isJson, storagePath);
break;
case 'get':
await handleGet(args.slice(1), isJson, storagePath);
break;
case 'restore':
await handleRestore(args.slice(1), isJson, storagePath);
break;
case 'delete':
await handleDelete(args.slice(1), isJson, storagePath);
break;
case 'export':
await handleExport(args.slice(1), isJson, storagePath);
break;
case 'import':
await handleImport(args.slice(1), isJson, storagePath);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleCreate(args: string[], options: Record<string, unknown>, isJson: boolean, storagePath?: string): Promise<void> {
const name = args[0] || `checkpoint-${Date.now()}`;
const mgr = getManager(storagePath);
// Set state from options if provided
const state = options.state ? JSON.parse(options.state as string) : {};
mgr.setState(state);
const checkpoint = await mgr.create(name, {
sessionId: options.session as string,
metadata: options.metadata ? JSON.parse(options.metadata as string) : undefined
});
if (isJson) {
console.log(JSON.stringify({ success: true, checkpoint }, null, 2));
} else {
console.log(`✓ Checkpoint created: ${checkpoint.id}`);
console.log(` Name: ${checkpoint.name}`);
console.log(` Time: ${checkpoint.timestamp.toISOString()}`);
}
}
async function handleList(isJson: boolean, storagePath?: string): Promise<void> {
const mgr = getManager(storagePath);
const checkpoints = await mgr.list();
if (isJson) {
console.log(JSON.stringify({ success: true, checkpoints }, null, 2));
} else {
if (checkpoints.length === 0) {
console.log('No checkpoints found');
} else {
console.log('Checkpoints:');
for (const cp of checkpoints) {
console.log(` ${cp.name} (${cp.id})`);
console.log(` Created: ${cp.timestamp.toISOString()}`);
if (cp.sessionId) console.log(` Session: ${cp.sessionId}`);
}
}
}
}
async function handleGet(args: string[], isJson: boolean, storagePath?: string): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Checkpoint ID is required');
process.exit(1);
}
const mgr = getManager(storagePath);
const checkpoint = await mgr.get(id);
if (!checkpoint) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: 'Checkpoint not found' }));
} else {
console.error(`Checkpoint ${id} not found`);
}
process.exit(1);
}
if (isJson) {
console.log(JSON.stringify({ success: true, checkpoint }, null, 2));
} else {
console.log(`Checkpoint: ${checkpoint.id}`);
console.log(` Name: ${checkpoint.name}`);
console.log(` Created: ${checkpoint.timestamp.toISOString()}`);
console.log(` State: ${JSON.stringify(checkpoint.state, null, 2)}`);
}
}
async function handleRestore(args: string[], isJson: boolean, storagePath?: string): Promise<void> {
const id = args[0];
const mgr = getManager(storagePath);
let checkpoint;
if (id) {
checkpoint = await mgr.restore(id);
} else {
checkpoint = await mgr.restoreLatest();
}
if (!checkpoint) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: 'No checkpoint to restore' }));
} else {
console.error('No checkpoint to restore');
}
process.exit(1);
}
if (isJson) {
console.log(JSON.stringify({ success: true, restored: checkpoint }, null, 2));
} else {
console.log(`✓ Restored checkpoint: ${checkpoint.id}`);
console.log(` Name: ${checkpoint.name}`);
console.log(` State keys: ${Object.keys(checkpoint.state).join(', ') || 'none'}`);
}
}
async function handleDelete(args: string[], isJson: boolean, storagePath?: string): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Checkpoint ID is required');
process.exit(1);
}
const mgr = getManager(storagePath);
await mgr.delete(id);
if (isJson) {
console.log(JSON.stringify({ success: true, deleted: id }));
} else {
console.log(`✓ Checkpoint ${id} deleted`);
}
}
async function handleExport(args: string[], isJson: boolean, storagePath?: string): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Checkpoint ID is required');
process.exit(1);
}
const mgr = getManager(storagePath);
const exported = await mgr.export(id);
if (!exported) {
console.error(`Checkpoint ${id} not found`);
process.exit(1);
}
console.log(exported);
}
async function handleImport(args: string[], isJson: boolean, storagePath?: string): Promise<void> {
const jsonData = args[0];
if (!jsonData) {
console.error('Error: JSON data is required');
process.exit(1);
}
const mgr = getManager(storagePath);
const checkpoint = await mgr.import(jsonData);
if (isJson) {
console.log(JSON.stringify({ success: true, imported: checkpoint }, null, 2));
} else {
console.log(`✓ Checkpoint imported: ${checkpoint.id}`);
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'checkpoints',
description: 'Session state checkpointing and recovery',
subcommands: {
create: 'Create a new checkpoint',
list: 'List all checkpoints',
get: 'Get checkpoint details',
restore: 'Restore from a checkpoint',
delete: 'Delete a checkpoint',
export: 'Export checkpoint as JSON',
import: 'Import checkpoint from JSON'
},
flags: {
'--storage': 'Path to checkpoint storage directory',
'--state': 'JSON state to save with checkpoint',
'--session': 'Session ID to associate',
'--metadata': 'JSON metadata',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts checkpoints create my-checkpoint',
'praisonai-ts checkpoints create --state \'{"key": "value"}\'',
'praisonai-ts checkpoints list',
'praisonai-ts checkpoints restore <id>',
'praisonai-ts checkpoints restore # restores latest',
'praisonai-ts checkpoints export <id> > checkpoint.json',
'praisonai-ts checkpoints import \'{"name": "imported"}\'',
'praisonai-ts checkpoints delete <id>'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Checkpoints - Session state checkpointing\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
@@ -0,0 +1,164 @@
/**
* Context command - Manage conversation context
*/
import { createContextAgent } from '../../agent/context';
import { resolveConfig } from '../config/resolve';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface ContextOptions {
model?: string;
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
maxMessages?: number;
}
export async function execute(args: string[], options: ContextOptions): Promise<void> {
const action = args[0] || 'help';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
const config = resolveConfig(options);
try {
switch (action) {
case 'chat':
await chatWithContext(actionArgs, options, config, outputFormat);
break;
case 'summarize':
await summarizeContext(actionArgs, options, config, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function chatWithContext(args: string[], options: ContextOptions, config: any, outputFormat: string): Promise<void> {
const message = args.join(' ');
if (!message) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a message'));
} else {
await pretty.error('Please provide a message');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
if (outputFormat !== 'json') {
await pretty.info('Processing with context...');
}
const agent = createContextAgent({
llm: config.model,
instructions: 'You are a helpful assistant with context awareness.',
contextWindow: options.maxMessages || 10,
verbose: options.verbose
});
const result = await agent.chat(message);
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess({
message,
response: result.text
}, {
duration_ms: duration,
model: config.model
}));
} else {
await pretty.heading('Response');
await pretty.plain(result.text);
await pretty.newline();
await pretty.success(`Completed in ${duration}ms`);
}
}
async function summarizeContext(args: string[], options: ContextOptions, config: any, outputFormat: string): Promise<void> {
const context = args.join(' ');
if (!context) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide context to summarize'));
} else {
await pretty.error('Please provide context to summarize');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
if (outputFormat !== 'json') {
await pretty.info('Summarizing context...');
}
const agent = createContextAgent({
llm: config.model,
instructions: 'You are a summarization assistant. Provide concise summaries.',
verbose: options.verbose
});
// Use chat to summarize
const result = await agent.chat(`Please summarize the following text concisely:\n\n${context}`);
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess({
originalLength: context.length,
summary: result.text
}, {
duration_ms: duration,
model: config.model
}));
} else {
await pretty.heading('Summary');
await pretty.plain(result.text);
await pretty.newline();
await pretty.dim(`Original: ${context.length} chars`);
await pretty.success(`Summarized in ${duration}ms`);
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'context',
subcommands: [
{ name: 'chat <message>', description: 'Chat with context management' },
{ name: 'summarize <text>', description: 'Summarize context' },
{ name: 'help', description: 'Show this help' }
],
flags: [
{ name: '--max-messages', description: 'Maximum messages to keep in context' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Context Command');
await pretty.plain('Manage conversation context\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(25)} ${cmd.description}`);
}
await pretty.newline();
await pretty.plain('Flags:');
for (const flag of help.flags) {
await pretty.plain(` ${flag.name.padEnd(20)} ${flag.description}`);
}
}
}
+153
View File
@@ -0,0 +1,153 @@
/**
* CLI command: cost
* Token usage and cost tracking
*/
import { CostTracker, createCostTracker, MODEL_PRICING, formatCost } from '../features/cost-tracker';
let tracker: CostTracker | null = null;
function getTracker(): CostTracker {
if (!tracker) {
tracker = createCostTracker();
}
return tracker;
}
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
switch (subcommand) {
case 'summary':
await handleSummary(isJson);
break;
case 'add':
await handleAdd(args.slice(1), options, isJson);
break;
case 'reset':
await handleReset(isJson);
break;
case 'pricing':
await handlePricing(args.slice(1), isJson);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleSummary(isJson: boolean): Promise<void> {
const t = getTracker();
const stats = t.getStats();
const breakdown = t.getBreakdownByModel();
if (isJson) {
console.log(JSON.stringify({ success: true, stats, breakdown }, null, 2));
} else {
console.log(t.getSummary());
if (Object.keys(breakdown).length > 0) {
console.log('\nBreakdown by model:');
for (const [model, data] of Object.entries(breakdown)) {
console.log(` ${model}: ${data.tokens} tokens, ${formatCost(data.cost)}, ${data.requests} requests`);
}
}
}
}
async function handleAdd(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const model = args[0] || 'gpt-4o-mini';
const inputTokens = parseInt(args[1] || '0');
const outputTokens = parseInt(args[2] || '0');
if (isNaN(inputTokens) || isNaN(outputTokens)) {
console.error('Error: Invalid token counts');
process.exit(1);
}
const t = getTracker();
const request = t.addUsage(model, inputTokens, outputTokens);
if (isJson) {
console.log(JSON.stringify({ success: true, request }, null, 2));
} else {
console.log(`✓ Added usage: ${model}`);
console.log(` Input: ${inputTokens} tokens`);
console.log(` Output: ${outputTokens} tokens`);
console.log(` Cost: ${formatCost(request.cost)}`);
}
}
async function handleReset(isJson: boolean): Promise<void> {
const t = getTracker();
t.reset();
if (isJson) {
console.log(JSON.stringify({ success: true, message: 'Cost tracker reset' }));
} else {
console.log('✓ Cost tracker reset');
}
}
async function handlePricing(args: string[], isJson: boolean): Promise<void> {
const model = args[0];
if (model) {
const pricing = MODEL_PRICING[model] || MODEL_PRICING['default'];
if (isJson) {
console.log(JSON.stringify({ success: true, model, pricing }, null, 2));
} else {
console.log(`Pricing for ${model}:`);
console.log(` Input: $${pricing.inputCostPer1k}/1K tokens`);
console.log(` Output: $${pricing.outputCostPer1k}/1K tokens`);
}
} else {
if (isJson) {
console.log(JSON.stringify({ success: true, pricing: MODEL_PRICING }, null, 2));
} else {
console.log('Model Pricing (per 1K tokens):\n');
for (const [name, pricing] of Object.entries(MODEL_PRICING)) {
if (name === 'default') continue;
console.log(` ${name.padEnd(30)} in: $${pricing.inputCostPer1k.toFixed(5)} out: $${pricing.outputCostPer1k.toFixed(5)}`);
}
}
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'cost',
description: 'Token usage and cost tracking',
subcommands: {
summary: 'Show cost summary',
add: 'Add token usage',
reset: 'Reset cost tracker',
pricing: 'Show model pricing'
},
flags: {
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts cost summary',
'praisonai-ts cost add gpt-4o 1000 500',
'praisonai-ts cost pricing',
'praisonai-ts cost pricing gpt-4o',
'praisonai-ts cost reset'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Cost - Token usage and cost tracking\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
+123
View File
@@ -0,0 +1,123 @@
/**
* DB command - Database adapter management
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface DbOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: DbOptions): Promise<void> {
const action = args[0] || 'help';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'info':
await showInfo(outputFormat);
break;
case 'adapters':
await listAdapters(outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function showInfo(outputFormat: string): Promise<void> {
const info = {
feature: 'Database Adapters',
description: 'Database adapters for persistence and session storage',
adapters: [
{ name: 'SQLiteAdapter', description: 'SQLite database adapter' },
{ name: 'UpstashRedisAdapter', description: 'Upstash Redis adapter' },
{ name: 'MemoryRedisAdapter', description: 'In-memory Redis-compatible adapter' },
{ name: 'NeonPostgresAdapter', description: 'Neon PostgreSQL adapter' },
{ name: 'MemoryPostgresAdapter', description: 'In-memory PostgreSQL-compatible adapter' },
{ name: 'PostgresSessionStorage', description: 'PostgreSQL session storage' }
],
capabilities: [
'Store conversation history',
'Persist agent state',
'Session management',
'Message storage and retrieval',
'Trace logging'
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(info));
} else {
await pretty.heading('Database Adapters');
await pretty.plain(info.description);
await pretty.newline();
await pretty.plain('Available Adapters:');
for (const a of info.adapters) {
await pretty.plain(` • ${a.name}: ${a.description}`);
}
await pretty.newline();
await pretty.plain('Capabilities:');
for (const cap of info.capabilities) {
await pretty.plain(` • ${cap}`);
}
}
}
async function listAdapters(outputFormat: string): Promise<void> {
const adapters = [
{ name: 'sqlite', description: 'SQLite database', available: true },
{ name: 'redis', description: 'Redis (Upstash)', available: true },
{ name: 'postgres', description: 'PostgreSQL (Neon)', available: true },
{ name: 'memory', description: 'In-memory adapters', available: true }
];
if (outputFormat === 'json') {
outputJson(formatSuccess({ adapters }));
} else {
await pretty.heading('Database Adapters');
for (const a of adapters) {
const status = a.available ? '✓' : '✗';
await pretty.plain(` ${status} ${a.name.padEnd(15)} ${a.description}`);
}
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'db',
description: 'Database adapter management',
subcommands: [
{ name: 'info', description: 'Show database adapter information' },
{ name: 'adapters', description: 'List available database adapters' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Database Command');
await pretty.plain(help.description);
await pretty.newline();
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
}
}
+283
View File
@@ -0,0 +1,283 @@
/**
* Eval command - Evaluate agent performance
*/
import { Agent } from '../../agent';
import { resolveConfig } from '../config/resolve';
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES, normalizeError } from '../output/errors';
export interface EvalOptions {
// Accuracy subcommand
input?: string;
expected?: string;
iterations?: number;
// Performance subcommand
warmup?: number;
// Reliability subcommand
'expected-tools'?: string;
// Common
model?: string;
verbose?: boolean;
profile?: string;
config?: string;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
interface AccuracyResult {
iteration: number;
output: string;
score: number;
passed: boolean;
}
interface PerformanceResult {
iteration: number;
duration_ms: number;
}
export async function execute(args: string[], options: EvalOptions): Promise<void> {
const subcommand = args[0];
if (!subcommand || !['accuracy', 'performance', 'reliability'].includes(subcommand)) {
if (options.json || options.output === 'json') {
printError(ERROR_CODES.INVALID_ARGS, 'Please specify a subcommand: accuracy, performance, or reliability');
} else {
await pretty.error('Please specify a subcommand: accuracy, performance, or reliability');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const config = resolveConfig({
configPath: options.config,
profile: options.profile,
model: options.model,
verbose: options.verbose
});
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (subcommand) {
case 'accuracy':
await runAccuracyEval(options, config, outputFormat);
break;
case 'performance':
await runPerformanceEval(options, config, outputFormat);
break;
case 'reliability':
await runReliabilityEval(options, config, outputFormat);
break;
}
} catch (error) {
const cliError = normalizeError(error);
if (outputFormat === 'json') {
outputJson(formatError(cliError.code, cliError.message, cliError.details));
} else {
await pretty.error(cliError.message);
if (config.verbose && error instanceof Error && error.stack) {
await pretty.dim(error.stack);
}
}
process.exit(cliError.exitCode);
}
}
async function runAccuracyEval(
options: EvalOptions,
config: { model: string; verbose: boolean },
outputFormat: string
): Promise<void> {
if (!options.input) {
throw new Error('--input is required for accuracy evaluation');
}
if (!options.expected) {
throw new Error('--expected is required for accuracy evaluation');
}
const iterations = options.iterations || 1;
const results: AccuracyResult[] = [];
const startTime = Date.now();
const agent = new Agent({
name: 'Eval Agent',
instructions: 'You are a helpful AI assistant.',
llm: config.model,
verbose: config.verbose
});
for (let i = 0; i < iterations; i++) {
const output = await agent.start(options.input);
// Simple similarity check (in production, use LLM-as-judge)
const similarity = calculateSimilarity(output, options.expected);
const score = Math.round(similarity * 10);
results.push({
iteration: i + 1,
output,
score,
passed: score >= 7
});
}
const duration = Date.now() - startTime;
const avgScore = results.reduce((sum, r) => sum + r.score, 0) / results.length;
const passRate = results.filter(r => r.passed).length / results.length;
if (outputFormat === 'json') {
outputJson(formatSuccess(
{
type: 'accuracy',
results,
summary: {
avg_score: avgScore,
pass_rate: passRate,
iterations
}
},
{ duration_ms: duration, model: config.model }
));
} else {
await pretty.heading('Accuracy Evaluation Results');
await pretty.keyValue({
'Iterations': iterations,
'Average Score': avgScore.toFixed(2),
'Pass Rate': `${(passRate * 100).toFixed(1)}%`,
'Duration': `${duration}ms`
});
}
}
async function runPerformanceEval(
options: EvalOptions,
config: { model: string; verbose: boolean },
outputFormat: string
): Promise<void> {
const iterations = options.iterations || 10;
const warmup = options.warmup || 2;
const results: PerformanceResult[] = [];
const agent = new Agent({
name: 'Perf Agent',
instructions: 'You are a helpful AI assistant.',
llm: config.model,
verbose: false // Disable verbose for performance testing
});
const testPrompt = 'Say "hello" and nothing else.';
// Warmup runs
for (let i = 0; i < warmup; i++) {
await agent.start(testPrompt);
}
// Actual runs
for (let i = 0; i < iterations; i++) {
const start = Date.now();
await agent.start(testPrompt);
const duration = Date.now() - start;
results.push({
iteration: i + 1,
duration_ms: duration
});
}
const durations = results.map(r => r.duration_ms);
const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
const min = Math.min(...durations);
const max = Math.max(...durations);
const sorted = [...durations].sort((a, b) => a - b);
const p95 = sorted[Math.floor(sorted.length * 0.95)];
if (outputFormat === 'json') {
outputJson(formatSuccess(
{
type: 'performance',
results,
summary: {
avg_ms: avg,
min_ms: min,
max_ms: max,
p95_ms: p95,
iterations,
warmup_runs: warmup
}
},
{ model: config.model }
));
} else {
await pretty.heading('Performance Evaluation Results');
await pretty.keyValue({
'Iterations': iterations,
'Warmup Runs': warmup,
'Avg Duration': `${avg.toFixed(2)}ms`,
'Min Duration': `${min}ms`,
'Max Duration': `${max}ms`,
'P95 Duration': `${p95}ms`
});
}
}
async function runReliabilityEval(
options: EvalOptions,
config: { model: string; verbose: boolean },
outputFormat: string
): Promise<void> {
const expectedTools = options['expected-tools']?.split(',').map(t => t.trim()) || [];
if (expectedTools.length === 0) {
throw new Error('--expected-tools is required for reliability evaluation');
}
// For now, simulate reliability check
// In production, this would track actual tool calls
const results = {
expected: expectedTools,
called: expectedTools, // Simulated
missing: [] as string[],
unexpected: [] as string[]
};
const passed = results.missing.length === 0;
if (outputFormat === 'json') {
outputJson(formatSuccess({
type: 'reliability',
status: passed ? 'PASSED' : 'FAILED',
results
}));
} else {
await pretty.heading('Reliability Evaluation Results');
if (passed) {
await pretty.success('All expected tools were called');
} else {
await pretty.error(`Missing tool calls: ${results.missing.join(', ')}`);
}
}
}
/**
* Simple string similarity calculation
*/
function calculateSimilarity(str1: string, str2: string): number {
const s1 = str1.toLowerCase().trim();
const s2 = str2.toLowerCase().trim();
if (s1 === s2) return 1;
if (s1.includes(s2) || s2.includes(s1)) return 0.9;
// Simple word overlap
const words1 = new Set(s1.split(/\s+/));
const words2 = new Set(s2.split(/\s+/));
const intersection = [...words1].filter(w => words2.has(w));
const union = new Set([...words1, ...words2]);
return intersection.length / union.size;
}
@@ -0,0 +1,185 @@
/**
* CLI command: external-agents
* Integration with external AI CLI tools
*/
import {
getExternalAgentRegistry,
createExternalAgent,
ClaudeCodeAgent,
GeminiCliAgent,
CodexCliAgent,
AiderAgent
} from '../features/external-agents';
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
switch (subcommand) {
case 'list':
await handleList(isJson);
break;
case 'check':
await handleCheck(args.slice(1), isJson);
break;
case 'run':
await handleRun(args.slice(1), options, isJson);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleList(isJson: boolean): Promise<void> {
const registry = getExternalAgentRegistry();
const all = registry.list();
const available = await registry.getAvailable();
if (isJson) {
console.log(JSON.stringify({
success: true,
agents: all,
available
}, null, 2));
} else {
console.log('External Agents:\n');
for (const name of all) {
const status = available.includes(name) ? '✓' : '✗';
console.log(` ${status} ${name}`);
}
console.log(`\n${available.length}/${all.length} available`);
}
}
async function handleCheck(args: string[], isJson: boolean): Promise<void> {
const name = args[0];
if (!name) {
console.error('Error: Agent name is required');
process.exit(1);
}
const agent = createExternalAgent(name);
if (!agent) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: `Unknown agent: ${name}` }));
} else {
console.error(`Unknown agent: ${name}`);
}
process.exit(1);
}
const available = await agent.isAvailable();
if (isJson) {
console.log(JSON.stringify({ success: true, agent: name, available }));
} else {
if (available) {
console.log(`✓ ${name} is available`);
} else {
console.log(`✗ ${name} is not available (CLI not found)`);
}
}
}
async function handleRun(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const name = args[0];
const prompt = args.slice(1).join(' ');
if (!name) {
console.error('Error: Agent name is required');
process.exit(1);
}
if (!prompt) {
console.error('Error: Prompt is required');
process.exit(1);
}
const cwd = options.cwd as string | undefined;
const agent = createExternalAgent(name, cwd);
if (!agent) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: `Unknown agent: ${name}` }));
} else {
console.error(`Unknown agent: ${name}`);
}
process.exit(1);
}
if (!await agent.isAvailable()) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: `${name} CLI not available` }));
} else {
console.error(`${name} CLI not available`);
}
process.exit(1);
}
const result = await agent.execute(prompt);
if (isJson) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.success) {
console.log(result.output);
} else {
console.error(`Error: ${result.error}`);
process.exit(result.exitCode);
}
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'external-agents',
description: 'Integration with external AI CLI tools',
subcommands: {
list: 'List all external agents and availability',
check: 'Check if an agent is available',
run: 'Run a prompt with an external agent'
},
agents: {
claude: 'Claude Code CLI',
gemini: 'Gemini CLI',
codex: 'OpenAI Codex CLI',
aider: 'Aider CLI'
},
flags: {
'--cwd': 'Working directory for the agent',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts external-agents list',
'praisonai-ts external-agents check claude',
'praisonai-ts external-agents run claude "Explain this code"',
'praisonai-ts external-agents run gemini "Refactor this function" --cwd ./src'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('External Agents - AI CLI tool integration\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nSupported Agents:');
for (const [agent, desc] of Object.entries(help.agents)) {
console.log(` ${agent.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
@@ -0,0 +1,136 @@
/**
* CLI command: fast-context
* Fast context retrieval and summarization
*/
import { FastContext, createFastContext, getQuickContext } from '../features/fast-context';
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
switch (subcommand) {
case 'query':
await handleQuery(args.slice(1), options, isJson);
break;
case 'stats':
await handleStats(isJson);
break;
case 'clear':
await handleClear(isJson);
break;
case 'help':
default:
showHelp(isJson);
}
}
let globalContext: FastContext | null = null;
function getContext(maxTokens?: number): FastContext {
if (!globalContext) {
globalContext = createFastContext({ maxTokens, cacheEnabled: true });
}
return globalContext;
}
async function handleQuery(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const query = args.join(' ');
if (!query) {
console.error('Error: Query is required');
process.exit(1);
}
const maxTokens = options['max-tokens'] as number | undefined;
const sources = options.sources as string | undefined;
const fc = getContext(maxTokens);
// Add sources if provided
if (sources) {
const sourceList = sources.split(',').map(s => s.trim());
fc.registerSource('cli-sources', 'custom', sourceList);
}
const result = await fc.getContext(query);
if (isJson) {
console.log(JSON.stringify({ success: true, ...result }, null, 2));
} else {
console.log('Fast Context Result:');
console.log(` Query: ${query}`);
console.log(` Token count: ${result.tokenCount}`);
console.log(` Sources used: ${result.sources.length}`);
console.log(` Cached: ${result.cached}`);
console.log(` Latency: ${result.latencyMs}ms`);
console.log('\nContext:');
console.log(result.context || '(empty)');
}
}
async function handleStats(isJson: boolean): Promise<void> {
const fc = getContext();
const stats = fc.getCacheStats();
if (isJson) {
console.log(JSON.stringify({ success: true, stats }, null, 2));
} else {
console.log('Fast Context Cache Stats:');
console.log(` Cache size: ${stats.size}`);
console.log(` Total hits: ${stats.totalHits}`);
}
}
async function handleClear(isJson: boolean): Promise<void> {
const fc = getContext();
fc.clearCache();
fc.clearSources();
if (isJson) {
console.log(JSON.stringify({ success: true, message: 'Cache and sources cleared' }));
} else {
console.log('✓ Cache and sources cleared');
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'fast-context',
description: 'Fast context retrieval and summarization',
subcommands: {
query: 'Query for relevant context',
stats: 'Show cache statistics',
clear: 'Clear cache and sources'
},
flags: {
'--max-tokens': 'Maximum tokens in context (default: 4000)',
'--sources': 'Comma-separated source texts',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts fast-context query "What is the main topic?"',
'praisonai-ts fast-context query "summarize" --sources "text1,text2,text3"',
'praisonai-ts fast-context query "find relevant" --max-tokens 2000',
'praisonai-ts fast-context stats',
'praisonai-ts fast-context clear'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Fast Context - Fast context retrieval\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(14)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
+146
View File
@@ -0,0 +1,146 @@
/**
* CLI command: flow
* Workflow flow visualization
*/
import { FlowDisplay, createFlowDisplay, renderWorkflow } from '../features/flow-display';
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
switch (subcommand) {
case 'show':
await handleShow(args.slice(1), options, isJson);
break;
case 'dot':
await handleDot(args.slice(1), options, isJson);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleShow(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const stepsStr = args[0] || options.steps as string;
let steps: Array<{ name: string; type?: string }> = [];
if (stepsStr) {
try {
steps = JSON.parse(stepsStr);
} catch {
// Parse simple format: "step1,step2,step3"
steps = stepsStr.split(',').map(s => {
const [name, type] = s.split(':');
return { name: name.trim(), type: type?.trim() };
});
}
} else {
// Default example
steps = [
{ name: 'Input', type: 'start' },
{ name: 'Process', type: 'agent' },
{ name: 'Validate', type: 'condition' },
{ name: 'Output', type: 'end' }
];
}
const display = createFlowDisplay({
showStatus: Boolean(options.status),
compact: Boolean(options.compact)
});
display.fromWorkflowSteps(steps);
if (isJson) {
const graph = display.getGraph();
console.log(JSON.stringify({
success: true,
nodes: Array.from(graph.nodes.values()),
edges: graph.edges
}, null, 2));
} else {
if (options.boxes) {
console.log(display.renderBoxes());
} else {
console.log(display.render());
}
}
}
async function handleDot(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const stepsStr = args[0] || options.steps as string;
let steps: Array<{ name: string; type?: string }> = [];
if (stepsStr) {
try {
steps = JSON.parse(stepsStr);
} catch {
steps = stepsStr.split(',').map(s => {
const [name, type] = s.split(':');
return { name: name.trim(), type: type?.trim() };
});
}
} else {
steps = [
{ name: 'Start', type: 'start' },
{ name: 'Agent', type: 'agent' },
{ name: 'End', type: 'end' }
];
}
const display = createFlowDisplay();
display.fromWorkflowSteps(steps);
const dot = display.toDot();
if (isJson) {
console.log(JSON.stringify({ success: true, dot }));
} else {
console.log(dot);
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'flow',
description: 'Workflow flow visualization',
subcommands: {
show: 'Display workflow as text tree or boxes',
dot: 'Export workflow as DOT format (for Graphviz)'
},
flags: {
'--steps': 'Steps as JSON or "name:type,name:type"',
'--status': 'Show status indicators',
'--compact': 'Compact display mode',
'--boxes': 'Display as ASCII boxes',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts flow show "Input,Process,Output"',
'praisonai-ts flow show "Start:start,Agent:agent,End:end"',
'praisonai-ts flow show --boxes',
'praisonai-ts flow dot "Step1,Step2,Step3" > workflow.dot'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Flow - Workflow visualization\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
+192
View File
@@ -0,0 +1,192 @@
/**
* CLI command: git
* Git integration for repository operations
*/
import { GitManager, createGitManager, DiffViewer, createDiffViewer } from '../features/git-integration';
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
const cwd = (options.cwd as string) || process.cwd();
switch (subcommand) {
case 'status':
await handleStatus(cwd, isJson);
break;
case 'diff':
await handleDiff(options, cwd, isJson);
break;
case 'log':
await handleLog(options, cwd, isJson);
break;
case 'branches':
await handleBranches(cwd, isJson);
break;
case 'stash':
await handleStash(args.slice(1), cwd, isJson);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleStatus(cwd: string, isJson: boolean): Promise<void> {
const git = createGitManager({ cwd, safe: true });
if (!await git.isRepo()) {
console.error('Error: Not a git repository');
process.exit(1);
}
const status = await git.getStatus();
if (isJson) {
console.log(JSON.stringify({ success: true, ...status }, null, 2));
} else {
console.log(await git.getStatusString());
}
}
async function handleDiff(options: Record<string, unknown>, cwd: string, isJson: boolean): Promise<void> {
const git = createGitManager({ cwd, safe: true });
const staged = Boolean(options.staged);
if (!await git.isRepo()) {
console.error('Error: Not a git repository');
process.exit(1);
}
const diff = await git.getDiff(staged);
if (isJson) {
console.log(JSON.stringify({ success: true, ...diff }, null, 2));
} else {
if (diff.files.length === 0) {
console.log(staged ? 'No staged changes' : 'No changes');
} else {
console.log(`Changes (${diff.files.length} files):\n`);
for (const file of diff.files) {
console.log(` ${file.path} (+${file.additions}/-${file.deletions})`);
}
if (options.full) {
console.log('\n' + diff.summary);
}
}
}
}
async function handleLog(options: Record<string, unknown>, cwd: string, isJson: boolean): Promise<void> {
const git = createGitManager({ cwd, safe: true });
const limit = (options.limit as number) || 10;
if (!await git.isRepo()) {
console.error('Error: Not a git repository');
process.exit(1);
}
const commits = await git.getLog(limit);
if (isJson) {
console.log(JSON.stringify({ success: true, commits }, null, 2));
} else {
console.log('Recent commits:\n');
for (const commit of commits) {
console.log(` ${commit.shortHash} ${commit.message}`);
console.log(` by ${commit.author} on ${commit.date.toLocaleDateString()}`);
}
}
}
async function handleBranches(cwd: string, isJson: boolean): Promise<void> {
const git = createGitManager({ cwd, safe: true });
if (!await git.isRepo()) {
console.error('Error: Not a git repository');
process.exit(1);
}
const branches = await git.getBranches();
if (isJson) {
console.log(JSON.stringify({ success: true, branches }, null, 2));
} else {
console.log('Branches:\n');
for (const branch of branches) {
const marker = branch.current ? '* ' : ' ';
console.log(`${marker}${branch.name}`);
}
}
}
async function handleStash(args: string[], cwd: string, isJson: boolean): Promise<void> {
const git = createGitManager({ cwd, safe: true });
if (!await git.isRepo()) {
console.error('Error: Not a git repository');
process.exit(1);
}
const stashes = await git.getStashList();
if (isJson) {
console.log(JSON.stringify({ success: true, stashes }, null, 2));
} else {
if (stashes.length === 0) {
console.log('No stashes');
} else {
console.log('Stashes:\n');
for (const stash of stashes) {
console.log(` ${stash}`);
}
}
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'git',
description: 'Git integration for repository operations (read-only by default)',
subcommands: {
status: 'Show repository status',
diff: 'Show changes',
log: 'Show recent commits',
branches: 'List branches',
stash: 'List stashes'
},
flags: {
'--cwd': 'Working directory (default: current)',
'--staged': 'Show staged changes only (for diff)',
'--full': 'Show full diff output',
'--limit': 'Number of commits to show (default: 10)',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts git status',
'praisonai-ts git diff',
'praisonai-ts git diff --staged',
'praisonai-ts git log --limit 5',
'praisonai-ts git branches',
'praisonai-ts git stash'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Git - Repository operations (read-only)\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
@@ -0,0 +1,104 @@
/**
* Graph RAG command - Graph-based retrieval augmented generation
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface GraphRagOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: GraphRagOptions): Promise<void> {
const action = args[0] || 'help';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'info':
await showInfo(outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function showInfo(outputFormat: string): Promise<void> {
const info = {
feature: 'Graph RAG',
description: 'Graph-based retrieval augmented generation for complex knowledge queries',
capabilities: [
'Build knowledge graphs from documents',
'Query relationships between entities',
'Combine graph traversal with vector search',
'Extract entities and relationships',
'Support for complex multi-hop queries'
],
sdkUsage: `
import { createGraphRAG, GraphStore } from 'praisonai';
// Create a graph RAG instance
const graphRag = createGraphRAG({
llm: 'openai/gpt-4o-mini',
graphStore: new GraphStore()
});
// Add documents
await graphRag.addDocument('Document content...');
// Query with graph context
const result = await graphRag.query('What is the relationship between X and Y?');
`
};
if (outputFormat === 'json') {
outputJson(formatSuccess(info));
} else {
await pretty.heading('Graph RAG');
await pretty.plain(info.description);
await pretty.newline();
await pretty.plain('Capabilities:');
for (const cap of info.capabilities) {
await pretty.plain(` • ${cap}`);
}
await pretty.newline();
await pretty.dim('Use the SDK for full Graph RAG functionality');
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'graph-rag',
description: 'Graph-based retrieval augmented generation',
subcommands: [
{ name: 'info', description: 'Show Graph RAG feature information' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Graph RAG Command');
await pretty.plain(help.description);
await pretty.newline();
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
}
}
@@ -0,0 +1,132 @@
/**
* Guardrail command - Content validation and safety
*/
import { createLLMGuardrail } from '../../guardrails/llm-guardrail';
import { resolveConfig } from '../config/resolve';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface GuardrailOptions {
model?: string;
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
criteria?: string;
}
export async function execute(args: string[], options: GuardrailOptions): Promise<void> {
const action = args[0] || 'help';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
const config = resolveConfig(options);
try {
switch (action) {
case 'check':
await checkContent(actionArgs, options, config, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function checkContent(args: string[], options: GuardrailOptions, config: any, outputFormat: string): Promise<void> {
const content = args.join(' ');
if (!content) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide content to check'));
} else {
await pretty.error('Please provide content to check');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
if (outputFormat !== 'json') {
await pretty.info('Checking content...');
}
const guardrail = createLLMGuardrail({
name: 'cli-guardrail',
llm: config.model,
criteria: options.criteria || 'Content should be safe, appropriate, and helpful',
verbose: options.verbose
});
const result = await guardrail.check(content);
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess({
content: content.substring(0, 100) + (content.length > 100 ? '...' : ''),
status: result.status,
score: result.score,
message: result.message,
reasoning: result.reasoning
}, {
duration_ms: duration,
model: config.model
}));
} else {
await pretty.heading('Guardrail Check');
if (result.status === 'passed') {
await pretty.success('Content passed guardrail check');
} else if (result.status === 'warning') {
await pretty.warn('Content has warnings');
} else {
await pretty.error('Content failed guardrail check');
}
await pretty.plain(`Score: ${(result.score * 10).toFixed(1)}/10`);
if (result.message) {
await pretty.plain(`Message: ${result.message}`);
}
if (result.reasoning) {
await pretty.dim(`Reasoning: ${result.reasoning}`);
}
await pretty.newline();
await pretty.dim(`Checked in ${duration}ms`);
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'guardrail',
subcommands: [
{ name: 'check <content>', description: 'Check content against guardrails' },
{ name: 'help', description: 'Show this help' }
],
flags: [
{ name: '--criteria', description: 'Custom criteria for validation' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Guardrail Command');
await pretty.plain('Content validation and safety checks\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(25)} ${cmd.description}`);
}
await pretty.newline();
await pretty.plain('Flags:');
for (const flag of help.flags) {
await pretty.plain(` ${flag.name.padEnd(20)} ${flag.description}`);
}
}
}
@@ -0,0 +1,106 @@
/**
* Handoff command - Agent handoff management
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface HandoffOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: HandoffOptions): Promise<void> {
const action = args[0] || 'help';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'info':
await showInfo(outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function showInfo(outputFormat: string): Promise<void> {
const info = {
feature: 'Handoff',
description: 'Agent handoff allows seamless transfer of conversation context between agents',
capabilities: [
'Transfer conversation to specialized agents',
'Preserve context during handoff',
'Filter handoffs based on conditions',
'Chain multiple agent handoffs'
],
sdkUsage: `
import { Handoff, handoff, handoffFilters } from 'praisonai';
// Create a handoff
const myHandoff = handoff({
target: specialistAgent,
condition: (ctx) => ctx.input.includes('technical'),
onHandoff: (ctx) => console.log('Handing off...')
});
// Use with an agent
const agent = new Agent({
handoffs: [myHandoff]
});
`
};
if (outputFormat === 'json') {
outputJson(formatSuccess(info));
} else {
await pretty.heading('Handoff Feature');
await pretty.plain(info.description);
await pretty.newline();
await pretty.plain('Capabilities:');
for (const cap of info.capabilities) {
await pretty.plain(` • ${cap}`);
}
await pretty.newline();
await pretty.dim('Use the SDK for programmatic handoff configuration');
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'handoff',
description: 'Agent handoff management - transfer conversations between agents',
subcommands: [
{ name: 'info', description: 'Show handoff feature information' },
{ name: 'help', description: 'Show this help' }
],
note: 'Handoff is primarily a programmatic SDK feature. Use the SDK for full functionality.'
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Handoff Command');
await pretty.plain(help.description);
await pretty.newline();
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
await pretty.newline();
await pretty.dim(help.note);
}
}
+151
View File
@@ -0,0 +1,151 @@
/**
* Help command - Show help information
*/
import { COMMANDS, GLOBAL_FLAGS, CLI_SPEC_VERSION, getCommandSpec } from '../spec/cli-spec';
import { outputJson, formatSuccess } from '../output/json';
import * as pretty from '../output/pretty';
export interface HelpOptions {
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: HelpOptions): Promise<void> {
const command = args[0];
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
if (command) {
await showCommandHelp(command, outputFormat);
} else {
await showGeneralHelp(outputFormat);
}
}
async function showGeneralHelp(outputFormat: string): Promise<void> {
const commands = Object.entries(COMMANDS).map(([name, spec]) => ({
name,
description: spec.description
}));
const flags = GLOBAL_FLAGS.map(f => ({
name: f.short ? `-${f.short}, --${f.name}` : `--${f.name}`,
description: f.description,
default: f.default
}));
if (outputFormat === 'json') {
outputJson(formatSuccess({
cli_spec_version: CLI_SPEC_VERSION,
commands,
global_flags: flags
}));
} else {
await pretty.plain(`
PraisonAI TypeScript CLI v${CLI_SPEC_VERSION}
Usage:
praisonai-ts <command> [options]
Commands:`);
for (const cmd of commands) {
await pretty.plain(` ${cmd.name.padEnd(12)} ${cmd.description}`);
}
await pretty.plain(`
Global Options:`);
for (const flag of flags) {
const defaultStr = flag.default !== undefined ? ` (default: ${flag.default})` : '';
await pretty.plain(` ${flag.name.padEnd(20)} ${flag.description}${defaultStr}`);
}
await pretty.plain(`
Environment Variables:
OPENAI_API_KEY OpenAI API key
ANTHROPIC_API_KEY Anthropic API key
GOOGLE_API_KEY Google API key
PRAISONAI_MODEL Default model
Examples:
praisonai-ts chat "Hello, how are you?"
praisonai-ts chat "Write a poem" --stream
praisonai-ts run "Analyze this data" --agent my-agent.yaml
praisonai-ts providers
praisonai-ts help chat
`);
}
}
async function showCommandHelp(command: string, outputFormat: string): Promise<void> {
const spec = getCommandSpec(command);
if (!spec) {
await pretty.error(`Unknown command: ${command}`);
await pretty.info('Run "praisonai-ts help" to see available commands');
return;
}
const args = spec.args || [];
const flags = [...GLOBAL_FLAGS, ...(spec.flags || [])];
const subcommands = spec.subcommands ? Object.entries(spec.subcommands) : [];
if (outputFormat === 'json') {
outputJson(formatSuccess({
command,
description: spec.description,
args: args.map(a => ({
name: a.name,
type: a.type,
required: a.required,
description: a.description
})),
flags: flags.map(f => ({
name: f.name,
short: f.short,
type: f.type,
default: f.default,
description: f.description
})),
subcommands: subcommands.map(([name, sub]) => ({
name,
description: sub.description
}))
}));
} else {
await pretty.plain(`
Command: ${command}
${spec.description}
Usage:
praisonai-ts ${command}${args.length ? ' ' + args.map(a => a.required ? `<${a.name}>` : `[${a.name}]`).join(' ') : ''} [options]`);
if (args.length > 0) {
await pretty.plain(`
Arguments:`);
for (const arg of args) {
const reqStr = arg.required ? ' (required)' : '';
await pretty.plain(` ${arg.name.padEnd(15)} ${arg.description || arg.type}${reqStr}`);
}
}
if (subcommands.length > 0) {
await pretty.plain(`
Subcommands:`);
for (const [name, sub] of subcommands) {
await pretty.plain(` ${name.padEnd(15)} ${sub.description}`);
}
}
await pretty.plain(`
Options:`);
for (const flag of flags) {
const shortStr = flag.short ? `-${flag.short}, ` : ' ';
const defaultStr = flag.default !== undefined ? ` (default: ${flag.default})` : '';
await pretty.plain(` ${shortStr}--${flag.name.padEnd(14)} ${flag.description || ''}${defaultStr}`);
}
await pretty.plain('');
}
}
+179
View File
@@ -0,0 +1,179 @@
/**
* Image command - Image generation and analysis
*/
import { createImageAgent } from '../../agent/image';
import { resolveConfig } from '../config/resolve';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface ImageOptions {
model?: string;
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
size?: string;
quality?: string;
style?: string;
}
export async function execute(args: string[], options: ImageOptions): Promise<void> {
const action = args[0] || 'help';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
const config = resolveConfig(options);
try {
switch (action) {
case 'generate':
await generateImage(actionArgs, options, config, outputFormat);
break;
case 'analyze':
await analyzeImage(actionArgs, options, config, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function generateImage(args: string[], options: ImageOptions, config: any, outputFormat: string): Promise<void> {
const prompt = args.join(' ');
if (!prompt) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a prompt for image generation'));
} else {
await pretty.error('Please provide a prompt for image generation');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
if (outputFormat !== 'json') {
await pretty.info(`Generating image: "${prompt}"`);
}
const agent = createImageAgent({
llm: config.model,
verbose: options.verbose
});
const result = await agent.generate({
prompt,
size: options.size as any,
quality: options.quality as any,
style: options.style as any
});
const duration = Date.now() - startTime;
const urls = Array.isArray(result) ? result : [result];
if (outputFormat === 'json') {
outputJson(formatSuccess({
prompt,
urls
}, {
duration_ms: duration,
model: config.model
}));
} else {
await pretty.heading('Generated Image');
for (const url of urls) {
await pretty.plain(`URL: ${url}`);
}
await pretty.newline();
await pretty.success(`Generated in ${duration}ms`);
}
}
async function analyzeImage(args: string[], options: ImageOptions, config: any, outputFormat: string): Promise<void> {
const imageUrl = args[0];
const prompt = args.slice(1).join(' ') || 'Describe this image';
if (!imageUrl) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide an image URL'));
} else {
await pretty.error('Please provide an image URL');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
if (outputFormat !== 'json') {
await pretty.info(`Analyzing image: ${imageUrl}`);
}
const agent = createImageAgent({
llm: config.model,
verbose: options.verbose
});
const result = await agent.analyze({
imageUrl,
prompt
});
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess({
imageUrl,
prompt,
analysis: result
}, {
duration_ms: duration,
model: config.model
}));
} else {
await pretty.heading('Image Analysis');
await pretty.plain(result);
await pretty.newline();
await pretty.success(`Analyzed in ${duration}ms`);
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'image',
subcommands: [
{ name: 'generate <prompt>', description: 'Generate an image from text' },
{ name: 'analyze <url> [question]', description: 'Analyze an image' },
{ name: 'help', description: 'Show this help' }
],
flags: [
{ name: '--size', description: 'Image size (1024x1024, 1792x1024, 1024x1792)' },
{ name: '--quality', description: 'Image quality (standard, hd)' },
{ name: '--style', description: 'Image style (vivid, natural)' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Image Command');
await pretty.plain('Generate and analyze images\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(30)} ${cmd.description}`);
}
await pretty.newline();
await pretty.plain('Flags:');
for (const flag of help.flags) {
await pretty.plain(` ${flag.name.padEnd(20)} ${flag.description}`);
}
}
}
@@ -0,0 +1,84 @@
/**
* CLI command: interactive
* Interactive TUI mode with slash commands
*/
import { InteractiveTUI, createInteractiveTUI } from '../features/interactive-tui';
import { createCostTracker } from '../features/cost-tracker';
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const isJson = Boolean(options.output === 'json' || options.json);
if (isJson) {
console.log(JSON.stringify({
success: false,
error: 'Interactive mode does not support JSON output'
}));
process.exit(1);
}
const historyFile = options.history as string | undefined;
const model = options.model as string | undefined;
console.log('Starting interactive mode...');
console.log('Type /help for available commands, /exit to quit.\n');
const tui = createInteractiveTUI({
prompt: options.prompt as string || '> ',
historyFile,
welcomeMessage: `PraisonAI Interactive Mode
Model: ${model || 'default'}
Type /help for commands, /exit to quit.`,
onMessage: async (message) => {
// Simple echo for now - in real usage this would call the agent
return `You said: ${message}`;
},
onExit: () => {
console.log('\nGoodbye!');
}
});
// Update context with model if provided
if (model) {
tui.updateContext({ model });
}
try {
await tui.start();
} catch (error: any) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
}
export function showHelp(): void {
console.log(`Interactive - Interactive TUI mode
Usage:
praisonai-ts interactive [options]
Options:
--model, -m Model to use
--prompt Custom prompt string (default: "> ")
--history Path to history file
Slash Commands (in interactive mode):
/help Show available commands
/cost Show token usage and cost
/clear Clear conversation history
/model [name] Show or change model
/tokens Show token count
/plan Manage task plan
/undo Undo last action
/diff Show pending changes
/commit Commit pending changes
/settings Show or modify settings
/map Show repository map
/exit Exit interactive mode
Examples:
praisonai-ts interactive
praisonai-ts interactive --model gpt-4o
praisonai-ts interactive --history ~/.praisonai_history
`);
}
+295
View File
@@ -0,0 +1,295 @@
/**
* CLI command: jobs
* Background job queue management
*/
import { JobQueue, createJobQueue, createFileJobStorage } from '../features/background-jobs';
let globalQueue: JobQueue | null = null;
function getQueue(storagePath?: string): JobQueue {
if (!globalQueue) {
const storage = storagePath ? createFileJobStorage(storagePath) : undefined;
globalQueue = createJobQueue({ storage });
}
return globalQueue;
}
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
const storagePath = options.storage as string | undefined;
switch (subcommand) {
case 'add':
await handleAdd(args.slice(1), options, isJson, storagePath);
break;
case 'list':
await handleList(options, isJson, storagePath);
break;
case 'get':
await handleGet(args.slice(1), isJson, storagePath);
break;
case 'cancel':
await handleCancel(args.slice(1), isJson, storagePath);
break;
case 'retry':
await handleRetry(args.slice(1), isJson, storagePath);
break;
case 'process':
await handleProcess(args.slice(1), isJson, storagePath);
break;
case 'stats':
await handleStats(isJson, storagePath);
break;
case 'cleanup':
await handleCleanup(options, isJson, storagePath);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleAdd(args: string[], options: Record<string, unknown>, isJson: boolean, storagePath?: string): Promise<void> {
const name = args[0];
const dataStr = args[1] || '{}';
if (!name) {
console.error('Error: Job name is required');
process.exit(1);
}
let data: Record<string, unknown>;
try {
data = JSON.parse(dataStr);
} catch {
data = { input: dataStr };
}
const priority = (options.priority as string) || 'normal';
const queue = getQueue(storagePath);
const job = await queue.add(name, data, {
priority: priority as 'low' | 'normal' | 'high' | 'critical'
});
if (isJson) {
console.log(JSON.stringify({ success: true, job }, null, 2));
} else {
console.log(`✓ Job created: ${job.id}`);
console.log(` Name: ${job.name}`);
console.log(` Priority: ${job.priority}`);
console.log(` Status: ${job.status}`);
}
}
async function handleList(options: Record<string, unknown>, isJson: boolean, storagePath?: string): Promise<void> {
const queue = getQueue(storagePath);
const status = options.status as string | undefined;
let jobs = await queue.getAll();
if (status) {
jobs = jobs.filter(j => j.status === status);
}
if (isJson) {
console.log(JSON.stringify({ success: true, jobs }, null, 2));
} else {
if (jobs.length === 0) {
console.log('No jobs found');
} else {
console.log('Jobs:');
for (const job of jobs) {
console.log(` ${job.id}`);
console.log(` Name: ${job.name}`);
console.log(` Status: ${job.status}`);
console.log(` Priority: ${job.priority}`);
console.log(` Attempts: ${job.attempts}/${job.maxAttempts}`);
if (job.error) console.log(` Error: ${job.error}`);
}
}
}
}
async function handleGet(args: string[], isJson: boolean, storagePath?: string): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Job ID is required');
process.exit(1);
}
const queue = getQueue(storagePath);
const job = await queue.get(id);
if (!job) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: 'Job not found' }));
} else {
console.error(`Job ${id} not found`);
}
process.exit(1);
}
if (isJson) {
console.log(JSON.stringify({ success: true, job }, null, 2));
} else {
console.log(`Job: ${job.id}`);
console.log(` Name: ${job.name}`);
console.log(` Status: ${job.status}`);
console.log(` Priority: ${job.priority}`);
console.log(` Attempts: ${job.attempts}/${job.maxAttempts}`);
console.log(` Created: ${job.createdAt}`);
if (job.startedAt) console.log(` Started: ${job.startedAt}`);
if (job.completedAt) console.log(` Completed: ${job.completedAt}`);
if (job.error) console.log(` Error: ${job.error}`);
if (job.result) console.log(` Result: ${JSON.stringify(job.result)}`);
}
}
async function handleCancel(args: string[], isJson: boolean, storagePath?: string): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Job ID is required');
process.exit(1);
}
const queue = getQueue(storagePath);
const cancelled = await queue.cancel(id);
if (isJson) {
console.log(JSON.stringify({ success: cancelled, id }));
} else {
console.log(cancelled ? `✓ Job ${id} cancelled` : `Could not cancel job ${id}`);
}
}
async function handleRetry(args: string[], isJson: boolean, storagePath?: string): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Job ID is required');
process.exit(1);
}
const queue = getQueue(storagePath);
const retried = await queue.retry(id);
if (isJson) {
console.log(JSON.stringify({ success: retried, id }));
} else {
console.log(retried ? `✓ Job ${id} queued for retry` : `Could not retry job ${id}`);
}
}
async function handleProcess(args: string[], isJson: boolean, storagePath?: string): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Job ID is required');
process.exit(1);
}
const queue = getQueue(storagePath);
// Register a simple handler
queue.register('default', async (job) => {
return { processed: true, data: job.data };
});
try {
const result = await queue.processNow(id);
if (isJson) {
console.log(JSON.stringify({ success: true, result }, null, 2));
} else {
console.log(`✓ Job ${id} processed`);
console.log(` Result: ${JSON.stringify(result)}`);
}
} catch (error: any) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: error.message }));
} else {
console.error(`Error: ${error.message}`);
}
process.exit(1);
}
}
async function handleStats(isJson: boolean, storagePath?: string): Promise<void> {
const queue = getQueue(storagePath);
const stats = await queue.getStats();
if (isJson) {
console.log(JSON.stringify({ success: true, stats }, null, 2));
} else {
console.log('Job Queue Stats:');
console.log(` Pending: ${stats.pending}`);
console.log(` Running: ${stats.running}`);
console.log(` Completed: ${stats.completed}`);
console.log(` Failed: ${stats.failed}`);
console.log(` Cancelled: ${stats.cancelled}`);
}
}
async function handleCleanup(options: Record<string, unknown>, isJson: boolean, storagePath?: string): Promise<void> {
const queue = getQueue(storagePath);
const days = (options.days as number) || 7;
const olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
const count = await queue.cleanup(olderThan);
if (isJson) {
console.log(JSON.stringify({ success: true, cleaned: count }));
} else {
console.log(`✓ Cleaned up ${count} jobs older than ${days} days`);
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'jobs',
description: 'Background job queue management',
subcommands: {
add: 'Add a new job to the queue',
list: 'List all jobs',
get: 'Get job details',
cancel: 'Cancel a pending job',
retry: 'Retry a failed job',
process: 'Process a job immediately',
stats: 'Show queue statistics',
cleanup: 'Clean up old completed/failed jobs'
},
flags: {
'--storage': 'Path to job storage file',
'--priority': 'Job priority (low, normal, high, critical)',
'--status': 'Filter by status',
'--days': 'Days for cleanup threshold',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts jobs add my-job \'{"input": "data"}\'',
'praisonai-ts jobs add task --priority high',
'praisonai-ts jobs list --status pending',
'praisonai-ts jobs get <job-id>',
'praisonai-ts jobs cancel <job-id>',
'praisonai-ts jobs stats',
'praisonai-ts jobs cleanup --days 30'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Jobs - Background job queue management\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
@@ -0,0 +1,203 @@
/**
* Knowledge command - Manage knowledge base
*/
import { BaseKnowledgeBase, Knowledge } from '../../knowledge';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
import * as fs from 'fs';
import * as path from 'path';
import { randomUUID } from 'crypto';
export interface KnowledgeOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
// Singleton knowledge base instance
let kbInstance: BaseKnowledgeBase | null = null;
function getKnowledgeBase(): BaseKnowledgeBase {
if (!kbInstance) {
kbInstance = new BaseKnowledgeBase();
}
return kbInstance;
}
export async function execute(args: string[], options: KnowledgeOptions): Promise<void> {
const action = args[0] || 'help';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
const kb = getKnowledgeBase();
switch (action) {
case 'add':
await addKnowledge(kb, actionArgs, outputFormat);
break;
case 'search':
await searchKnowledge(kb, actionArgs, outputFormat);
break;
case 'list':
await listKnowledge(kb, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function addKnowledge(kb: BaseKnowledgeBase, args: string[], outputFormat: string): Promise<void> {
const source = args[0];
if (!source) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a file path or text content'));
} else {
await pretty.error('Please provide a file path or text content');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
let content: string;
let sourceName: string;
// Check if it's a file path
if (fs.existsSync(source)) {
content = fs.readFileSync(source, 'utf-8');
sourceName = path.basename(source);
} else {
// Treat as direct text content
content = args.join(' ');
sourceName = 'text-input';
}
const knowledge: Knowledge = {
id: randomUUID(),
type: 'text',
content,
metadata: { source: sourceName, addedAt: new Date().toISOString() }
};
kb.addKnowledge(knowledge);
if (outputFormat === 'json') {
outputJson(formatSuccess({
added: true,
id: knowledge.id,
source: sourceName,
contentLength: content.length
}));
} else {
await pretty.success(`Knowledge added from: ${sourceName}`);
await pretty.dim(`Content length: ${content.length} characters`);
}
}
async function searchKnowledge(kb: BaseKnowledgeBase, args: string[], outputFormat: string): Promise<void> {
const query = args.join(' ');
if (!query) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a search query'));
} else {
await pretty.error('Please provide a search query');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const results = kb.searchKnowledge(query);
if (outputFormat === 'json') {
outputJson(formatSuccess({
query,
results: results.map(r => ({
id: r.id,
content: r.content,
metadata: r.metadata
})),
count: results.length
}));
} else {
await pretty.heading(`Search Results for: "${query}"`);
if (results.length === 0) {
await pretty.info('No matching knowledge found');
} else {
for (const result of results) {
const contentStr = typeof result.content === 'string' ? result.content : JSON.stringify(result.content);
await pretty.plain(` • ${contentStr.substring(0, 100)}${contentStr.length > 100 ? '...' : ''}`);
if (result.metadata?.source) {
await pretty.dim(` Source: ${result.metadata.source}`);
}
}
}
await pretty.newline();
await pretty.info(`Found: ${results.length} results`);
}
}
async function listKnowledge(kb: BaseKnowledgeBase, outputFormat: string): Promise<void> {
// Search with empty string to get all
const entries = kb.searchKnowledge('');
if (outputFormat === 'json') {
outputJson(formatSuccess({
entries: entries.map((e: Knowledge) => ({
id: e.id,
type: e.type,
contentPreview: typeof e.content === 'string' ? e.content.substring(0, 100) : JSON.stringify(e.content).substring(0, 100),
source: e.metadata?.source
})),
count: entries.length
}));
} else {
await pretty.heading('Knowledge Base Entries');
if (entries.length === 0) {
await pretty.info('No knowledge entries found');
} else {
for (const entry of entries) {
const contentStr = typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content);
await pretty.plain(` • ${contentStr.substring(0, 80)}${contentStr.length > 80 ? '...' : ''}`);
if (entry.metadata?.source) {
await pretty.dim(` Source: ${entry.metadata.source}`);
}
}
}
await pretty.newline();
await pretty.info(`Total: ${entries.length} entries`);
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'knowledge',
subcommands: [
{ name: 'add <file|text>', description: 'Add knowledge from file or text' },
{ name: 'search <query>', description: 'Search knowledge base' },
{ name: 'list', description: 'List all knowledge entries' },
{ name: 'clear', description: 'Clear all knowledge' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Knowledge Command');
await pretty.plain('Manage knowledge base for RAG\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(25)} ${cmd.description}`);
}
}
}
+230
View File
@@ -0,0 +1,230 @@
/**
* MCP command - Model Context Protocol management
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface MCPOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
// MCP server registry (in-memory for now)
interface MCPServer {
name: string;
command: string;
args?: string[];
env?: Record<string, string>;
status: 'stopped' | 'running';
}
const mcpServers: Map<string, MCPServer> = new Map();
export async function execute(args: string[], options: MCPOptions): Promise<void> {
const action = args[0] || 'list';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'list':
await listServers(outputFormat);
break;
case 'add':
await addServer(actionArgs, outputFormat);
break;
case 'remove':
await removeServer(actionArgs, outputFormat);
break;
case 'start':
await startServer(actionArgs, outputFormat);
break;
case 'stop':
await stopServer(actionArgs, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function listServers(outputFormat: string): Promise<void> {
const servers = Array.from(mcpServers.values());
if (outputFormat === 'json') {
outputJson(formatSuccess({
servers,
count: servers.length
}));
} else {
await pretty.heading('MCP Servers');
if (servers.length === 0) {
await pretty.info('No MCP servers registered');
await pretty.dim('Run "praisonai-ts mcp add <name> <command>" to add a server');
} else {
for (const server of servers) {
const statusIcon = server.status === 'running' ? '🟢' : '⚪';
await pretty.plain(` ${statusIcon} ${server.name}`);
await pretty.dim(` Command: ${server.command} ${(server.args || []).join(' ')}`);
}
}
await pretty.newline();
await pretty.info(`Total: ${servers.length} servers`);
}
}
async function addServer(args: string[], outputFormat: string): Promise<void> {
const name = args[0];
const command = args[1];
const serverArgs = args.slice(2);
if (!name || !command) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide server name and command'));
} else {
await pretty.error('Please provide server name and command');
await pretty.dim('Usage: praisonai-ts mcp add <name> <command> [args...]');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const server: MCPServer = {
name,
command,
args: serverArgs.length > 0 ? serverArgs : undefined,
status: 'stopped'
};
mcpServers.set(name, server);
if (outputFormat === 'json') {
outputJson(formatSuccess({ added: true, server }));
} else {
await pretty.success(`MCP server added: ${name}`);
await pretty.dim(`Command: ${command} ${serverArgs.join(' ')}`);
}
}
async function removeServer(args: string[], outputFormat: string): Promise<void> {
const name = args[0];
if (!name) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide server name'));
} else {
await pretty.error('Please provide server name');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const removed = mcpServers.delete(name);
if (outputFormat === 'json') {
outputJson(formatSuccess({ removed, name }));
} else {
if (removed) {
await pretty.success(`MCP server removed: ${name}`);
} else {
await pretty.warn(`Server not found: ${name}`);
}
}
}
async function startServer(args: string[], outputFormat: string): Promise<void> {
const name = args[0];
if (!name) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide server name'));
} else {
await pretty.error('Please provide server name');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const server = mcpServers.get(name);
if (!server) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, `Server not found: ${name}`));
} else {
await pretty.error(`Server not found: ${name}`);
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
// Mark as running (actual process management would be more complex)
server.status = 'running';
if (outputFormat === 'json') {
outputJson(formatSuccess({ started: true, server }));
} else {
await pretty.success(`MCP server started: ${name}`);
}
}
async function stopServer(args: string[], outputFormat: string): Promise<void> {
const name = args[0];
if (!name) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide server name'));
} else {
await pretty.error('Please provide server name');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const server = mcpServers.get(name);
if (!server) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, `Server not found: ${name}`));
} else {
await pretty.error(`Server not found: ${name}`);
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
server.status = 'stopped';
if (outputFormat === 'json') {
outputJson(formatSuccess({ stopped: true, server }));
} else {
await pretty.success(`MCP server stopped: ${name}`);
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'mcp',
description: 'Model Context Protocol server management',
subcommands: [
{ name: 'list', description: 'List registered MCP servers' },
{ name: 'add <name> <cmd> [args]', description: 'Add an MCP server' },
{ name: 'remove <name>', description: 'Remove an MCP server' },
{ name: 'start <name>', description: 'Start an MCP server' },
{ name: 'stop <name>', description: 'Stop an MCP server' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('MCP Command');
await pretty.plain('Model Context Protocol server management\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(25)} ${cmd.description}`);
}
}
}
+174
View File
@@ -0,0 +1,174 @@
/**
* Memory command - Manage agent memory
*/
import { Memory, createMemory, MemoryEntry, SearchResult } from '../../memory/memory';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface MemoryOptions {
userId?: string;
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: MemoryOptions): Promise<void> {
const action = args[0] || 'list';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
const memory = createMemory();
switch (action) {
case 'list':
await listMemories(memory, outputFormat);
break;
case 'add':
await addMemory(memory, actionArgs, outputFormat);
break;
case 'search':
await searchMemory(memory, actionArgs, outputFormat);
break;
case 'clear':
await clearMemory(memory, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function listMemories(memory: Memory, outputFormat: string): Promise<void> {
const entries = memory.getAll();
if (outputFormat === 'json') {
outputJson(formatSuccess({
memories: entries,
count: entries.length
}));
} else {
await pretty.heading('Memory Entries');
if (entries.length === 0) {
await pretty.info('No memories stored');
} else {
for (const entry of entries) {
await pretty.plain(` • ${entry.content.substring(0, 100)}${entry.content.length > 100 ? '...' : ''}`);
if (entry.metadata) {
await pretty.dim(` Created: ${new Date(entry.timestamp).toISOString()}`);
}
}
}
await pretty.newline();
await pretty.info(`Total: ${entries.length} memories`);
}
}
async function addMemory(memory: Memory, args: string[], outputFormat: string): Promise<void> {
const content = args.join(' ');
if (!content) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide content to add'));
} else {
await pretty.error('Please provide content to add');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
await memory.add(content, 'user');
if (outputFormat === 'json') {
outputJson(formatSuccess({ added: content }));
} else {
await pretty.success('Memory added successfully');
}
}
async function searchMemory(memory: Memory, args: string[], outputFormat: string): Promise<void> {
const query = args.join(' ');
if (!query) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a search query'));
} else {
await pretty.error('Please provide a search query');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const results = await memory.search(query);
if (outputFormat === 'json') {
outputJson(formatSuccess({
query,
results,
count: results.length
}));
} else {
await pretty.heading(`Search Results for: "${query}"`);
if (results.length === 0) {
await pretty.info('No matching memories found');
} else {
for (const result of results) {
const content = result.entry.content;
await pretty.plain(` • ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`);
await pretty.dim(` Score: ${result.score.toFixed(3)}`);
}
}
await pretty.newline();
await pretty.info(`Found: ${results.length} results`);
}
}
async function clearMemory(memory: Memory, outputFormat: string): Promise<void> {
memory.clear();
if (outputFormat === 'json') {
outputJson(formatSuccess({ cleared: true }));
} else {
await pretty.success('Memory cleared successfully');
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'memory',
subcommands: [
{ name: 'list', description: 'List all memories' },
{ name: 'add <content>', description: 'Add a new memory' },
{ name: 'search <query>', description: 'Search memories' },
{ name: 'clear', description: 'Clear all memories' },
{ name: 'help', description: 'Show this help' }
],
options: [
{ name: '--user-id', description: 'User ID for memory isolation' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Memory Command');
await pretty.plain('Manage agent memory storage\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
await pretty.newline();
await pretty.plain('Options:');
for (const opt of help.options) {
await pretty.plain(` ${opt.name.padEnd(20)} ${opt.description}`);
}
}
}
+143
View File
@@ -0,0 +1,143 @@
/**
* CLI command: n8n
* N8N workflow integration
*/
import { N8NIntegration, createN8NIntegration } from '../features/n8n-integration';
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
switch (subcommand) {
case 'trigger':
await handleTrigger(args.slice(1), options, isJson);
break;
case 'export':
await handleExport(args.slice(1), options, isJson);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleTrigger(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const webhookId = args[0];
const payloadStr = args[1] || '{}';
if (!webhookId) {
console.error('Error: Webhook ID is required');
process.exit(1);
}
let payload: Record<string, unknown>;
try {
payload = JSON.parse(payloadStr);
} catch {
payload = { data: payloadStr };
}
const baseUrl = options['base-url'] as string | undefined;
const apiKey = options['api-key'] as string | undefined;
const n8n = createN8NIntegration({ baseUrl, apiKey });
const result = await n8n.triggerWebhook(webhookId, payload);
if (isJson) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.success) {
console.log(`✓ Webhook triggered: ${webhookId}`);
if (result.response) {
console.log('Response:', JSON.stringify(result.response, null, 2));
}
} else {
console.error(`✗ Webhook failed: ${result.error}`);
process.exit(1);
}
}
}
async function handleExport(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const name = args[0] || 'workflow';
const stepsStr = options.steps as string | undefined;
let steps: Array<{ name: string; type: 'agent' | 'tool' | 'condition' }> = [];
if (stepsStr) {
try {
steps = JSON.parse(stepsStr);
} catch {
// Parse simple format: "step1:agent,step2:tool"
steps = stepsStr.split(',').map(s => {
const [stepName, type] = s.split(':');
return { name: stepName, type: (type || 'agent') as 'agent' | 'tool' | 'condition' };
});
}
} else {
// Default example workflow
steps = [
{ name: 'Process Input', type: 'agent' },
{ name: 'Validate', type: 'condition' },
{ name: 'Execute', type: 'tool' }
];
}
const n8n = createN8NIntegration();
const workflow = n8n.exportWorkflowJSON(name, steps);
if (isJson || !options.pretty) {
console.log(workflow);
} else {
console.log(`N8N Workflow: ${name}\n`);
console.log('Steps:');
for (const step of steps) {
console.log(` - ${step.name} (${step.type})`);
}
console.log('\nExported workflow JSON:');
console.log(workflow);
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'n8n',
description: 'N8N workflow integration',
subcommands: {
trigger: 'Trigger an N8N webhook',
export: 'Export workflow to N8N format'
},
flags: {
'--base-url': 'N8N base URL (default: http://localhost:5678)',
'--api-key': 'N8N API key',
'--steps': 'Workflow steps as JSON or "name:type,name:type"',
'--pretty': 'Pretty print export',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts n8n trigger my-webhook \'{"data": "test"}\'',
'praisonai-ts n8n trigger webhook-id --base-url https://n8n.example.com',
'praisonai-ts n8n export my-workflow',
'praisonai-ts n8n export workflow --steps "Process:agent,Validate:condition"'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('N8N - Workflow integration\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
@@ -0,0 +1,119 @@
/**
* Observability command - Monitoring and tracing
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface ObservabilityOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: ObservabilityOptions): Promise<void> {
const action = args[0] || 'help';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'providers':
await listProviders(outputFormat);
break;
case 'info':
await showInfo(outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function showInfo(outputFormat: string): Promise<void> {
const info = {
feature: 'Observability',
description: 'Monitoring, tracing, and logging for agent operations',
providers: [
{ name: 'ConsoleObservabilityProvider', description: 'Console-based logging' },
{ name: 'MemoryObservabilityProvider', description: 'In-memory trace storage' },
{ name: 'LangfuseObservabilityProvider', description: 'Langfuse integration' }
],
capabilities: [
'Trace agent executions',
'Log LLM calls and responses',
'Track tool invocations',
'Measure performance metrics',
'Export traces to external systems'
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(info));
} else {
await pretty.heading('Observability');
await pretty.plain(info.description);
await pretty.newline();
await pretty.plain('Providers:');
for (const p of info.providers) {
await pretty.plain(` • ${p.name}: ${p.description}`);
}
await pretty.newline();
await pretty.plain('Capabilities:');
for (const cap of info.capabilities) {
await pretty.plain(` • ${cap}`);
}
}
}
async function listProviders(outputFormat: string): Promise<void> {
const providers = [
{ name: 'console', description: 'Console logging (default)', available: true },
{ name: 'memory', description: 'In-memory storage', available: true },
{ name: 'langfuse', description: 'Langfuse observability platform', available: true }
];
if (outputFormat === 'json') {
outputJson(formatSuccess({ providers }));
} else {
await pretty.heading('Observability Providers');
for (const p of providers) {
const status = p.available ? '✓' : '✗';
await pretty.plain(` ${status} ${p.name.padEnd(15)} ${p.description}`);
}
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'observability',
description: 'Monitoring and tracing for agent operations',
subcommands: [
{ name: 'info', description: 'Show observability feature information' },
{ name: 'providers', description: 'List available observability providers' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Observability Command');
await pretty.plain(help.description);
await pretty.newline();
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
}
}
@@ -0,0 +1,139 @@
/**
* Planning command - Task planning and todo management
*/
import { createPlan, createTodoList, TodoItem, type PlanConfig, type TodoItemConfig } from '../../planning';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface PlanningOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: PlanningOptions): Promise<void> {
const action = args[0] || 'help';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'create':
await createNewPlan(actionArgs, outputFormat);
break;
case 'todo':
await manageTodo(actionArgs, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function createNewPlan(args: string[], outputFormat: string): Promise<void> {
const name = args.join(' ');
if (!name) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a name for the plan'));
} else {
await pretty.error('Please provide a name for the plan');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const plan = createPlan({
name,
description: name
});
if (outputFormat === 'json') {
outputJson(formatSuccess({
id: plan.id,
name: plan.name,
status: plan.status
}));
} else {
await pretty.success(`Plan created: ${plan.id}`);
await pretty.plain(`Name: ${name}`);
}
}
async function manageTodo(args: string[], outputFormat: string): Promise<void> {
const subAction = args[0] || 'list';
const todoArgs = args.slice(1);
const todoList = createTodoList();
switch (subAction) {
case 'add':
const content = todoArgs.join(' ');
if (!content) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide todo content'));
} else {
await pretty.error('Please provide todo content');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const newItem = new TodoItem({ content });
todoList.add(newItem);
if (outputFormat === 'json') {
outputJson(formatSuccess({ added: true, id: newItem.id, content: newItem.content }));
} else {
await pretty.success(`Todo added: ${content}`);
}
break;
case 'list':
default:
const items = todoList.items;
if (outputFormat === 'json') {
outputJson(formatSuccess({ items, count: items.length }));
} else {
await pretty.heading('Todo List');
if (items.length === 0) {
await pretty.info('No todos');
} else {
for (const todo of items) {
const status = todo.status === 'completed' ? '✓' : '○';
await pretty.plain(` ${status} ${todo.content}`);
}
}
}
break;
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'planning',
subcommands: [
{ name: 'create <goal>', description: 'Create a new plan' },
{ name: 'todo add <title>', description: 'Add a todo item' },
{ name: 'todo list', description: 'List todo items' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Planning Command');
await pretty.plain('Task planning and todo management\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(25)} ${cmd.description}`);
}
}
}
@@ -0,0 +1,77 @@
/**
* Prompt Expand command - Expand prompts with more detail
*/
import { createPromptExpanderAgent } from '../../agent/prompt-expander';
import { resolveConfig } from '../config/resolve';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface PromptExpandOptions {
model?: string;
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
strategy?: string;
}
export async function execute(args: string[], options: PromptExpandOptions): Promise<void> {
const prompt = args.join(' ');
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
const config = resolveConfig(options);
if (!prompt) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a prompt to expand'));
} else {
await pretty.error('Please provide a prompt to expand');
await pretty.dim('Usage: praisonai-ts prompt-expand "your prompt"');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
try {
if (outputFormat !== 'json') {
await pretty.info(`Expanding prompt: "${prompt}"`);
}
const agent = createPromptExpanderAgent({
llm: config.model,
verbose: options.verbose,
defaultStrategy: options.strategy as any
});
const result = await agent.expand(prompt);
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess({
originalPrompt: prompt,
expandedPrompt: result.expanded,
strategy: result.strategy,
additions: result.additions
}, {
duration_ms: duration,
model: config.model
}));
} else {
await pretty.heading('Expanded Prompt');
await pretty.plain(result.expanded);
await pretty.newline();
await pretty.dim(`Strategy: ${result.strategy}`);
await pretty.success(`Completed in ${duration}ms`);
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
@@ -0,0 +1,84 @@
/**
* Providers command - List available LLM providers
*/
import { getAvailableProviders } from '../../llm/providers';
import { hasApiKey } from '../runtime/env';
import { outputJson, formatSuccess } from '../output/json';
import * as pretty from '../output/pretty';
export interface ProvidersOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
interface ProviderInfo {
name: string;
available: boolean;
hasApiKey: boolean;
models?: string[];
}
const PROVIDER_MODELS: Record<string, string[]> = {
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'],
anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-haiku-20240307'],
google: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash']
};
export async function execute(args: string[], options: ProvidersOptions): Promise<void> {
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
const availableProviders = getAvailableProviders();
const providers: ProviderInfo[] = [
{
name: 'openai',
available: availableProviders.includes('openai'),
hasApiKey: hasApiKey('openai'),
models: PROVIDER_MODELS.openai
},
{
name: 'anthropic',
available: availableProviders.includes('anthropic'),
hasApiKey: hasApiKey('anthropic'),
models: PROVIDER_MODELS.anthropic
},
{
name: 'google',
available: availableProviders.includes('google'),
hasApiKey: hasApiKey('google'),
models: PROVIDER_MODELS.google
}
];
if (outputFormat === 'json') {
outputJson(formatSuccess({
providers: providers.map(p => ({
name: p.name,
available: p.available,
has_api_key: p.hasApiKey,
models: options.verbose ? p.models : undefined
}))
}));
} else {
await pretty.heading('Available Providers');
for (const provider of providers) {
const status = provider.available ? '✅' : '❌';
const keyStatus = provider.hasApiKey ? '🔑' : '⚠️ No API key';
await pretty.plain(` ${status} ${provider.name} ${keyStatus}`);
if (options.verbose && provider.models) {
for (const model of provider.models) {
await pretty.dim(` - ${model}`);
}
}
}
await pretty.newline();
await pretty.info('Set API keys via environment variables:');
await pretty.dim(' OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY');
}
}
@@ -0,0 +1,80 @@
/**
* Query Rewrite command - Rewrite queries for better search results
*/
import { createQueryRewriterAgent } from '../../agent/query-rewriter';
import { resolveConfig } from '../config/resolve';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface QueryRewriteOptions {
model?: string;
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
strategy?: string;
count?: number;
}
export async function execute(args: string[], options: QueryRewriteOptions): Promise<void> {
const query = args.join(' ');
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
const config = resolveConfig(options);
if (!query) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a query to rewrite'));
} else {
await pretty.error('Please provide a query to rewrite');
await pretty.dim('Usage: praisonai-ts query-rewrite "your search query"');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
try {
if (outputFormat !== 'json') {
await pretty.info(`Rewriting query: "${query}"`);
}
const agent = createQueryRewriterAgent({
llm: config.model,
verbose: options.verbose,
defaultStrategy: options.strategy as any
});
const result = await agent.rewrite(query);
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess({
originalQuery: query,
rewrittenQueries: result.rewritten,
strategy: result.strategy,
confidence: result.confidence
}, {
duration_ms: duration,
model: config.model
}));
} else {
await pretty.heading('Rewritten Queries');
for (let i = 0; i < result.rewritten.length; i++) {
await pretty.plain(` ${i + 1}. ${result.rewritten[i]}`);
}
await pretty.newline();
await pretty.dim(`Strategy: ${result.strategy}`);
await pretty.success(`Completed in ${duration}ms`);
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
@@ -0,0 +1,154 @@
/**
* CLI command: repo-map
* Repository structure visualization and symbol extraction
*/
import { RepoMap, createRepoMap, getRepoTree } from '../features/repo-map';
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'tree';
const isJson = Boolean(options.output === 'json' || options.json);
switch (subcommand) {
case 'tree':
await handleTree(args.slice(1), options, isJson);
break;
case 'symbols':
await handleSymbols(args.slice(1), options, isJson);
break;
case 'help':
default:
if (subcommand !== 'help' && !subcommand.startsWith('-')) {
// Treat as path for tree command
await handleTree([subcommand, ...args.slice(1)], options, isJson);
} else {
showHelp(isJson);
}
}
}
async function handleTree(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const rootPath = args[0] || process.cwd();
const maxDepth = (options.depth as number) || 5;
const includeSymbols = Boolean(options.symbols);
const map = createRepoMap({
rootPath,
maxDepth,
includeSymbols
});
try {
const result = await map.generate();
if (isJson) {
console.log(JSON.stringify({
success: true,
root: result.root,
totalFiles: result.totalFiles,
totalDirectories: result.totalDirectories,
symbolCount: result.symbols.length
}, null, 2));
} else {
console.log(result.summary);
}
} catch (error: any) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: error.message }));
} else {
console.error(`Error: ${error.message}`);
}
process.exit(1);
}
}
async function handleSymbols(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const rootPath = args[0] || process.cwd();
const maxDepth = (options.depth as number) || 5;
const map = createRepoMap({
rootPath,
maxDepth,
includeSymbols: true
});
try {
const result = await map.generate();
if (isJson) {
console.log(JSON.stringify({
success: true,
symbols: result.symbols,
count: result.symbols.length
}, null, 2));
} else {
console.log(`Symbols in ${rootPath}:\n`);
const byType: Record<string, typeof result.symbols> = {};
for (const sym of result.symbols) {
if (!byType[sym.type]) byType[sym.type] = [];
byType[sym.type].push(sym);
}
for (const [type, symbols] of Object.entries(byType)) {
console.log(`${type}s (${symbols.length}):`);
for (const sym of symbols.slice(0, 20)) {
const exported = sym.exported ? ' [exported]' : '';
console.log(` ${sym.name}${exported} (line ${sym.line})`);
}
if (symbols.length > 20) {
console.log(` ... and ${symbols.length - 20} more`);
}
console.log();
}
}
} catch (error: any) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: error.message }));
} else {
console.error(`Error: ${error.message}`);
}
process.exit(1);
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'repo-map',
description: 'Repository structure visualization and symbol extraction',
subcommands: {
tree: 'Show repository tree structure',
symbols: 'Extract and list code symbols'
},
flags: {
'--depth': 'Maximum directory depth (default: 5)',
'--symbols': 'Include symbols in tree output',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts repo-map',
'praisonai-ts repo-map tree ./src',
'praisonai-ts repo-map tree --depth 3',
'praisonai-ts repo-map symbols ./src',
'praisonai-ts repo-map tree --symbols'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Repo Map - Repository visualization\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
@@ -0,0 +1,119 @@
/**
* Reranker command - Document reranking
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface RerankerOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
provider?: string;
}
export async function execute(args: string[], options: RerankerOptions): Promise<void> {
const action = args[0] || 'help';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'providers':
await listProviders(outputFormat);
break;
case 'info':
await showInfo(outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function showInfo(outputFormat: string): Promise<void> {
const info = {
feature: 'Reranker',
description: 'Rerank search results for improved relevance',
providers: [
{ name: 'CohereReranker', description: 'Cohere reranking API' },
{ name: 'CrossEncoderReranker', description: 'Cross-encoder model reranking' },
{ name: 'LLMReranker', description: 'LLM-based reranking' }
],
capabilities: [
'Rerank search results by relevance',
'Improve RAG retrieval quality',
'Multiple reranking strategies',
'Configurable top-k selection'
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(info));
} else {
await pretty.heading('Reranker');
await pretty.plain(info.description);
await pretty.newline();
await pretty.plain('Providers:');
for (const p of info.providers) {
await pretty.plain(` • ${p.name}: ${p.description}`);
}
await pretty.newline();
await pretty.plain('Capabilities:');
for (const cap of info.capabilities) {
await pretty.plain(` • ${cap}`);
}
}
}
async function listProviders(outputFormat: string): Promise<void> {
const providers = [
{ name: 'cohere', description: 'Cohere reranking API', available: true },
{ name: 'cross-encoder', description: 'Cross-encoder model', available: true },
{ name: 'llm', description: 'LLM-based reranking', available: true }
];
if (outputFormat === 'json') {
outputJson(formatSuccess({ providers }));
} else {
await pretty.heading('Reranker Providers');
for (const p of providers) {
const status = p.available ? '✓' : '✗';
await pretty.plain(` ${status} ${p.name.padEnd(15)} ${p.description}`);
}
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'reranker',
description: 'Document reranking for improved search relevance',
subcommands: [
{ name: 'info', description: 'Show reranker feature information' },
{ name: 'providers', description: 'List available reranker providers' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Reranker Command');
await pretty.plain(help.description);
await pretty.newline();
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
}
}
@@ -0,0 +1,88 @@
/**
* Research command - Deep research agent
*/
import { createDeepResearchAgent } from '../../agent/research';
import { resolveConfig } from '../config/resolve';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface ResearchOptions {
model?: string;
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
depth?: number;
maxSources?: number;
}
export async function execute(args: string[], options: ResearchOptions): Promise<void> {
const query = args.join(' ');
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
const config = resolveConfig(options);
if (!query) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a research query'));
} else {
await pretty.error('Please provide a research query');
await pretty.dim('Usage: praisonai-ts research "What are the latest AI trends?"');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
try {
if (outputFormat !== 'json') {
await pretty.info(`Researching: "${query}"`);
}
const agent = createDeepResearchAgent({
llm: config.model,
verbose: options.verbose,
maxIterations: options.depth
});
const result = await agent.research(query);
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess({
query,
answer: result.answer,
citations: result.citations,
reasoning: result.reasoning,
confidence: result.confidence
}, {
duration_ms: duration,
model: config.model
}));
} else {
await pretty.heading('Research Results');
await pretty.newline();
await pretty.plain(result.answer);
if (result.citations && result.citations.length > 0) {
await pretty.newline();
await pretty.plain('Citations:');
for (const citation of result.citations) {
await pretty.dim(` • ${citation.title || citation.url}`);
}
}
await pretty.newline();
await pretty.success(`Completed in ${duration}ms`);
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
+122
View File
@@ -0,0 +1,122 @@
/**
* Router command - Route requests to appropriate agents
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface RouterOptions {
model?: string;
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
routes?: string;
}
export async function execute(args: string[], options: RouterOptions): Promise<void> {
const action = args[0] || 'help';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'analyze':
await analyzeInput(actionArgs, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function analyzeInput(args: string[], outputFormat: string): Promise<void> {
const input = args.join(' ');
if (!input) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide input to analyze'));
} else {
await pretty.error('Please provide input to analyze');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const startTime = Date.now();
if (outputFormat !== 'json') {
await pretty.info(`Analyzing routing for: "${input}"`);
}
// Simple rule-based routing analysis
const routes = [
{ name: 'greeting', pattern: /\b(hello|hi|hey|greetings)\b/i, priority: 1 },
{ name: 'question', pattern: /\?$/, priority: 2 },
{ name: 'code', pattern: /\b(code|function|class|implement|debug)\b/i, priority: 3 },
{ name: 'research', pattern: /\b(research|analyze|investigate|study)\b/i, priority: 3 },
{ name: 'creative', pattern: /\b(write|create|generate|compose)\b/i, priority: 2 },
{ name: 'general', pattern: /.*/, priority: 0 }
];
const matches = routes
.filter(r => r.pattern.test(input))
.sort((a, b) => b.priority - a.priority);
const selectedRoute = matches[0]?.name || 'general';
const confidence = matches[0]?.priority ? Math.min(0.5 + matches[0].priority * 0.15, 0.95) : 0.5;
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess({
input,
selectedRoute,
confidence,
matchedRoutes: matches.map(m => m.name)
}, {
duration_ms: duration
}));
} else {
await pretty.heading('Routing Analysis');
await pretty.plain(`Input: "${input}"`);
await pretty.plain(`Selected Route: ${selectedRoute}`);
await pretty.plain(`Confidence: ${(confidence * 100).toFixed(1)}%`);
await pretty.plain(`Matched Routes: ${matches.map(m => m.name).join(', ')}`);
await pretty.newline();
await pretty.success(`Analyzed in ${duration}ms`);
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'router',
description: 'Route requests to appropriate agents based on content analysis',
subcommands: [
{ name: 'analyze <input>', description: 'Analyze input and suggest routing' },
{ name: 'help', description: 'Show this help' }
],
usage: 'For programmatic routing with actual agents, use the RouterAgent SDK class'
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Router Command');
await pretty.plain('Route requests to appropriate agents\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(25)} ${cmd.description}`);
}
await pretty.newline();
await pretty.dim('Note: For full routing with agents, use the RouterAgent SDK class');
}
}
+97
View File
@@ -0,0 +1,97 @@
/**
* Run command - Run an agent with a task
*/
import { Agent } from '../../agent';
import { resolveConfig } from '../config/resolve';
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES, normalizeError } from '../output/errors';
export interface RunOptions {
agent?: string;
tools?: string;
model?: string;
verbose?: boolean;
profile?: string;
config?: string;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: RunOptions): Promise<void> {
const task = args[0];
if (!task) {
if (options.json || options.output === 'json') {
printError(ERROR_CODES.MISSING_ARG, 'Please provide a task');
} else {
await pretty.error('Please provide a task');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
// Resolve config with precedence
const config = resolveConfig({
configPath: options.config,
profile: options.profile,
model: options.model,
verbose: options.verbose
});
const startTime = Date.now();
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
// Parse tools if provided
const tools: string[] = options.tools ? options.tools.split(',').map(t => t.trim()) : [];
// Create agent
const agent = new Agent({
name: 'CLI Agent',
instructions: 'You are a helpful AI assistant.',
llm: config.model,
verbose: config.verbose
});
// Execute task
const result = await agent.start(task);
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess(
{
result: result,
agent: 'CLI Agent',
task: task
},
{
duration_ms: duration,
model: config.model
}
));
} else {
if (typeof result === 'string') {
console.log(result);
} else {
console.log(JSON.stringify(result, null, 2));
}
}
} catch (error) {
const cliError = normalizeError(error);
if (outputFormat === 'json') {
outputJson(formatError(cliError.code, cliError.message, cliError.details));
} else {
await pretty.error(cliError.message);
if (config.verbose && error instanceof Error && error.stack) {
await pretty.dim(error.stack);
}
}
process.exit(cliError.exitCode);
}
}
@@ -0,0 +1,139 @@
/**
* CLI command: sandbox
* Safe command execution with restrictions
*/
import { SandboxExecutor, createSandboxExecutor, CommandValidator, type SandboxMode } from '../features/sandbox-executor';
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
switch (subcommand) {
case 'exec':
await handleExec(args.slice(1), options, isJson);
break;
case 'check':
await handleCheck(args.slice(1), isJson);
break;
case 'mode':
await handleMode(args.slice(1), isJson);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleExec(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const command = args.join(' ');
if (!command) {
console.error('Error: Command is required');
process.exit(1);
}
const mode = (options.mode as SandboxMode) || 'basic';
const timeout = (options.timeout as number) || 30000;
const executor = createSandboxExecutor({ mode, timeout });
const result = await executor.execute(command);
if (isJson) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.success) {
console.log('✓ Command executed successfully');
console.log(`Duration: ${result.duration}ms`);
if (result.stdout) {
console.log('\nOutput:');
console.log(result.stdout);
}
} else {
console.error('✗ Command failed');
if (result.stderr) console.error(result.stderr);
}
}
if (!result.success) process.exit(result.exitCode || 1);
}
async function handleCheck(args: string[], isJson: boolean): Promise<void> {
const command = args.join(' ');
if (!command) {
console.error('Error: Command is required');
process.exit(1);
}
const validator = new CommandValidator();
const result = validator.validate(command);
if (isJson) {
console.log(JSON.stringify({ command, ...result }, null, 2));
} else {
if (result.valid) {
console.log(`✓ Command is allowed: ${command}`);
} else {
console.log(`✗ Command blocked: ${command}`);
console.log(` Reason: ${result.reason}`);
}
}
}
async function handleMode(args: string[], isJson: boolean): Promise<void> {
const modes: Record<SandboxMode, string> = {
disabled: 'No execution allowed',
basic: 'Basic validation, blocks dangerous patterns',
strict: 'Minimal environment, restricted PATH',
'network-isolated': 'No network access (proxy blocked)'
};
if (isJson) {
console.log(JSON.stringify({ success: true, modes }, null, 2));
} else {
console.log('Sandbox Modes:\n');
for (const [mode, desc] of Object.entries(modes)) {
console.log(` ${mode.padEnd(18)} ${desc}`);
}
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'sandbox',
description: 'Safe command execution with restrictions',
subcommands: {
exec: 'Execute a command in sandbox',
check: 'Check if a command would be allowed',
mode: 'List available sandbox modes'
},
flags: {
'--mode': 'Sandbox mode (disabled, basic, strict, network-isolated)',
'--timeout': 'Execution timeout in ms (default: 30000)',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts sandbox exec "ls -la"',
'praisonai-ts sandbox exec "npm install" --mode strict',
'praisonai-ts sandbox check "rm -rf /"',
'praisonai-ts sandbox mode'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Sandbox - Safe command execution\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
@@ -0,0 +1,266 @@
/**
* CLI command: scheduler
* Agent task scheduling with cron-like patterns
*/
import { Scheduler, createScheduler, cronExpressions } from '../features/scheduler';
export async function execute(args: string[], options: Record<string, unknown>): Promise<void> {
const subcommand = args[0] || 'help';
const isJson = Boolean(options.output === 'json' || options.json);
switch (subcommand) {
case 'create':
await handleCreate(args.slice(1), options, isJson);
break;
case 'list':
await handleList(options, isJson);
break;
case 'run':
await handleRun(args.slice(1), options, isJson);
break;
case 'remove':
await handleRemove(args.slice(1), options, isJson);
break;
case 'enable':
await handleEnable(args.slice(1), options, isJson);
break;
case 'disable':
await handleDisable(args.slice(1), options, isJson);
break;
case 'patterns':
await handlePatterns(isJson);
break;
case 'help':
default:
showHelp(isJson);
}
}
async function handleCreate(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const name = args[0];
const cron = options.cron as string | undefined;
const interval = options.interval as number | undefined;
if (!name) {
const error = { error: 'Task name is required' };
if (isJson) {
console.log(JSON.stringify(error));
} else {
console.error('Error: Task name is required');
console.log('Usage: praisonai-ts scheduler create <name> --cron "* * * * *"');
}
process.exit(1);
}
if (!cron && !interval) {
const error = { error: 'Either --cron or --interval is required' };
if (isJson) {
console.log(JSON.stringify(error));
} else {
console.error('Error: Either --cron or --interval is required');
}
process.exit(1);
}
const scheduler = createScheduler();
const id = scheduler.add({
name,
cron,
interval: interval ? interval * 1000 : undefined,
task: async () => {
console.log(`Task ${name} executed at ${new Date().toISOString()}`);
return { executed: true };
}
});
const result = {
success: true,
id,
name,
cron,
interval,
message: `Task '${name}' created with ID: ${id}`
};
if (isJson) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`✓ Task '${name}' created`);
console.log(` ID: ${id}`);
if (cron) console.log(` Cron: ${cron}`);
if (interval) console.log(` Interval: ${interval}s`);
}
}
async function handleList(options: Record<string, unknown>, isJson: boolean): Promise<void> {
const scheduler = createScheduler();
const tasks = scheduler.getAllTasks();
if (isJson) {
console.log(JSON.stringify({ success: true, tasks }, null, 2));
} else {
if (tasks.length === 0) {
console.log('No scheduled tasks');
} else {
console.log('Scheduled Tasks:');
for (const task of tasks) {
console.log(` ${task.name} (${task.id})`);
console.log(` Status: ${task.status}`);
console.log(` Enabled: ${task.enabled}`);
console.log(` Run count: ${task.runCount}`);
if (task.nextRun) console.log(` Next run: ${task.nextRun.toISOString()}`);
}
}
}
}
async function handleRun(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Task ID is required');
process.exit(1);
}
const scheduler = createScheduler();
try {
const result = await scheduler.runNow(id);
if (isJson) {
console.log(JSON.stringify({ success: true, result }, null, 2));
} else {
console.log(`✓ Task ${id} executed successfully`);
}
} catch (error: any) {
if (isJson) {
console.log(JSON.stringify({ success: false, error: error.message }));
} else {
console.error(`Error: ${error.message}`);
}
process.exit(1);
}
}
async function handleRemove(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Task ID is required');
process.exit(1);
}
const scheduler = createScheduler();
const removed = scheduler.remove(id);
if (isJson) {
console.log(JSON.stringify({ success: removed, id }));
} else {
if (removed) {
console.log(`✓ Task ${id} removed`);
} else {
console.error(`Task ${id} not found`);
process.exit(1);
}
}
}
async function handleEnable(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Task ID is required');
process.exit(1);
}
const scheduler = createScheduler();
const enabled = scheduler.enable(id);
if (isJson) {
console.log(JSON.stringify({ success: enabled, id }));
} else {
console.log(enabled ? `✓ Task ${id} enabled` : `Task ${id} not found`);
}
}
async function handleDisable(args: string[], options: Record<string, unknown>, isJson: boolean): Promise<void> {
const id = args[0];
if (!id) {
console.error('Error: Task ID is required');
process.exit(1);
}
const scheduler = createScheduler();
const disabled = scheduler.disable(id);
if (isJson) {
console.log(JSON.stringify({ success: disabled, id }));
} else {
console.log(disabled ? `✓ Task ${id} disabled` : `Task ${id} not found`);
}
}
async function handlePatterns(isJson: boolean): Promise<void> {
const patterns = {
everyMinute: cronExpressions.everyMinute,
every5Minutes: cronExpressions.every5Minutes,
every15Minutes: cronExpressions.every15Minutes,
everyHour: cronExpressions.everyHour,
everyDay: cronExpressions.everyDay,
everyWeek: cronExpressions.everyWeek,
everyMonth: cronExpressions.everyMonth,
weekdays: cronExpressions.weekdays,
weekends: cronExpressions.weekends
};
if (isJson) {
console.log(JSON.stringify({ success: true, patterns }, null, 2));
} else {
console.log('Common Cron Patterns:');
for (const [name, pattern] of Object.entries(patterns)) {
console.log(` ${name}: ${pattern}`);
}
}
}
function showHelp(isJson: boolean): void {
const help = {
command: 'scheduler',
description: 'Agent task scheduling with cron-like patterns',
subcommands: {
create: 'Create a scheduled task',
list: 'List all scheduled tasks',
run: 'Run a task immediately',
remove: 'Remove a scheduled task',
enable: 'Enable a task',
disable: 'Disable a task',
patterns: 'Show common cron patterns'
},
flags: {
'--cron': 'Cron expression (e.g., "*/5 * * * *")',
'--interval': 'Interval in seconds',
'--json': 'Output in JSON format'
},
examples: [
'praisonai-ts scheduler create my-task --cron "*/5 * * * *"',
'praisonai-ts scheduler create backup --interval 3600',
'praisonai-ts scheduler list',
'praisonai-ts scheduler run <task-id>',
'praisonai-ts scheduler patterns'
]
};
if (isJson) {
console.log(JSON.stringify(help, null, 2));
} else {
console.log('Scheduler - Agent task scheduling\n');
console.log('Subcommands:');
for (const [cmd, desc] of Object.entries(help.subcommands)) {
console.log(` ${cmd.padEnd(12)} ${desc}`);
}
console.log('\nFlags:');
for (const [flag, desc] of Object.entries(help.flags)) {
console.log(` ${flag.padEnd(12)} ${desc}`);
}
console.log('\nExamples:');
for (const ex of help.examples) {
console.log(` ${ex}`);
}
}
}
@@ -0,0 +1,214 @@
/**
* Session command - Manage agent sessions
*/
import { Session, SessionManager, getSessionManager } from '../../session';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface SessionOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: SessionOptions): Promise<void> {
const action = args[0] || 'list';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
const manager = getSessionManager();
switch (action) {
case 'list':
await listSessions(manager, outputFormat);
break;
case 'create':
await createSession(manager, actionArgs, outputFormat);
break;
case 'get':
await getSession(manager, actionArgs, outputFormat);
break;
case 'delete':
await deleteSession(manager, actionArgs, outputFormat);
break;
case 'export':
await exportSession(manager, actionArgs, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function listSessions(manager: SessionManager, outputFormat: string): Promise<void> {
const sessions = manager.list();
if (outputFormat === 'json') {
outputJson(formatSuccess({
sessions: sessions.map((s: Session) => ({
id: s.id,
messageCount: s.messages.length,
createdAt: s.createdAt
})),
count: sessions.length
}));
} else {
await pretty.heading('Sessions');
if (sessions.length === 0) {
await pretty.info('No sessions found');
} else {
for (const session of sessions) {
await pretty.plain(` • ${session.id}`);
await pretty.dim(` Messages: ${session.messages.length}`);
}
}
await pretty.newline();
await pretty.info(`Total: ${sessions.length} sessions`);
}
}
async function createSession(manager: SessionManager, args: string[], outputFormat: string): Promise<void> {
const sessionId = args[0];
const session = manager.create({ id: sessionId });
if (outputFormat === 'json') {
outputJson(formatSuccess({
created: true,
sessionId: session.id
}));
} else {
await pretty.success(`Session created: ${session.id}`);
}
}
async function getSession(manager: SessionManager, args: string[], outputFormat: string): Promise<void> {
const sessionId = args[0];
if (!sessionId) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a session ID'));
} else {
await pretty.error('Please provide a session ID');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const session = manager.get(sessionId);
if (!session) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, `Session not found: ${sessionId}`));
} else {
await pretty.error(`Session not found: ${sessionId}`);
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
const messages = session.messages;
if (outputFormat === 'json') {
outputJson(formatSuccess({
id: session.id,
messages,
messageCount: messages.length
}));
} else {
await pretty.heading(`Session: ${session.id}`);
await pretty.plain(`Messages: ${messages.length}`);
await pretty.newline();
for (const msg of messages.slice(-10)) {
const content = msg.content || '';
await pretty.plain(` [${msg.role}] ${content.substring(0, 80)}${content.length > 80 ? '...' : ''}`);
}
}
}
async function deleteSession(manager: SessionManager, args: string[], outputFormat: string): Promise<void> {
const sessionId = args[0];
if (!sessionId) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a session ID'));
} else {
await pretty.error('Please provide a session ID');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const deleted = manager.delete(sessionId);
if (outputFormat === 'json') {
outputJson(formatSuccess({ deleted, sessionId }));
} else {
if (deleted) {
await pretty.success(`Session deleted: ${sessionId}`);
} else {
await pretty.warn(`Session not found: ${sessionId}`);
}
}
}
async function exportSession(manager: SessionManager, args: string[], outputFormat: string): Promise<void> {
const sessionId = args[0];
if (!sessionId) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a session ID'));
} else {
await pretty.error('Please provide a session ID');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const session = manager.get(sessionId);
if (!session) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, `Session not found: ${sessionId}`));
} else {
await pretty.error(`Session not found: ${sessionId}`);
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
const exported = session.toJSON();
if (outputFormat === 'json') {
outputJson(formatSuccess(exported));
} else {
console.log(JSON.stringify(exported, null, 2));
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'session',
subcommands: [
{ name: 'list', description: 'List all sessions' },
{ name: 'create [id]', description: 'Create a new session' },
{ name: 'get <id>', description: 'Get session details' },
{ name: 'delete <id>', description: 'Delete a session' },
{ name: 'export <id>', description: 'Export session data' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Session Command');
await pretty.plain('Manage agent sessions\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
}
}
+228
View File
@@ -0,0 +1,228 @@
/**
* Skills command - Manage agent skills
*/
import { SkillManager, createSkillManager, Skill } from '../../skills';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface SkillsOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: SkillsOptions): Promise<void> {
const action = args[0] || 'list';
const actionArgs = args.slice(1);
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
const searchPath = actionArgs[0] || '.';
const manager = createSkillManager({ paths: [searchPath] });
switch (action) {
case 'list':
await listSkills(manager, outputFormat);
break;
case 'discover':
await discoverSkills(manager, actionArgs, outputFormat);
break;
case 'validate':
await validateSkill(manager, actionArgs, outputFormat);
break;
case 'info':
await skillInfo(manager, actionArgs, outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function listSkills(manager: SkillManager, outputFormat: string): Promise<void> {
const skills = manager.list();
if (outputFormat === 'json') {
outputJson(formatSuccess({
skills: skills.map((s: Skill) => ({
name: s.metadata.name,
description: s.metadata.description,
path: s.path
})),
count: skills.length
}));
} else {
await pretty.heading('Available Skills');
if (skills.length === 0) {
await pretty.info('No skills loaded');
await pretty.dim('Run "praisonai-ts skills discover <path>" to discover skills');
} else {
for (const skill of skills) {
await pretty.plain(` • ${skill.metadata.name}`);
if (skill.metadata.description) {
await pretty.dim(` ${skill.metadata.description}`);
}
}
}
await pretty.newline();
await pretty.info(`Total: ${skills.length} skills`);
}
}
async function discoverSkills(manager: SkillManager, args: string[], outputFormat: string): Promise<void> {
const searchPath = args[0] || '.';
const discovered = await manager.discover();
if (outputFormat === 'json') {
outputJson(formatSuccess({
discovered: discovered.map((s: Skill) => ({
name: s.metadata.name,
description: s.metadata.description,
path: s.path
})),
count: discovered.length,
searchPath
}));
} else {
await pretty.heading(`Discovered Skills in: ${searchPath}`);
if (discovered.length === 0) {
await pretty.info('No skills found');
} else {
for (const skill of discovered) {
await pretty.plain(` • ${skill.metadata.name}`);
if (skill.metadata.description) {
await pretty.dim(` ${skill.metadata.description}`);
}
if (skill.path) {
await pretty.dim(` Path: ${skill.path}`);
}
}
}
await pretty.newline();
await pretty.info(`Found: ${discovered.length} skills`);
}
}
async function validateSkill(manager: SkillManager, args: string[], outputFormat: string): Promise<void> {
const skillPath = args[0];
if (!skillPath) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a skill path'));
} else {
await pretty.error('Please provide a skill path');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
try {
// Load the skill first, then validate
const skill = await manager.loadSkill(skillPath);
const result = manager.validate(skill);
if (outputFormat === 'json') {
outputJson(formatSuccess({
valid: result.valid,
path: skillPath,
errors: result.errors || []
}));
} else {
if (result.valid) {
await pretty.success(`Skill is valid: ${skillPath}`);
} else {
await pretty.error(`Skill validation failed: ${skillPath}`);
if (result.errors) {
for (const err of result.errors) {
await pretty.plain(` ❌ ${err}`);
}
}
}
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(`Validation error: ${error instanceof Error ? error.message : String(error)}`);
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function skillInfo(manager: SkillManager, args: string[], outputFormat: string): Promise<void> {
const skillName = args[0];
if (!skillName) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a skill name'));
} else {
await pretty.error('Please provide a skill name');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
const skill = manager.get(skillName);
if (!skill) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, `Skill not found: ${skillName}`));
} else {
await pretty.error(`Skill not found: ${skillName}`);
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
if (outputFormat === 'json') {
outputJson(formatSuccess({
name: skill.metadata.name,
description: skill.metadata.description,
path: skill.path,
metadata: skill.metadata
}));
} else {
await pretty.heading(`Skill: ${skill.metadata.name}`);
await pretty.keyValue({
'Name': skill.metadata.name,
'Description': skill.metadata.description || 'No description',
'Path': skill.path || 'N/A'
});
if (skill.metadata.metadata && Object.keys(skill.metadata.metadata).length > 0) {
await pretty.newline();
await pretty.plain('Metadata:');
await pretty.plain(JSON.stringify(skill.metadata.metadata, null, 2));
}
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'skills',
subcommands: [
{ name: 'list', description: 'List loaded skills' },
{ name: 'discover <path>', description: 'Discover skills in directory' },
{ name: 'validate <path>', description: 'Validate a skill' },
{ name: 'info <name>', description: 'Show skill information' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Skills Command');
await pretty.plain('Manage agent skills\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
}
}
@@ -0,0 +1,117 @@
/**
* Telemetry command - Usage monitoring and analytics
*/
import { getTelemetry, enableTelemetry, disableTelemetry, cleanupTelemetry } from '../../telemetry';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface TelemetryOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: TelemetryOptions): Promise<void> {
const action = args[0] || 'status';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'enable':
enableTelemetry();
if (outputFormat === 'json') {
outputJson(formatSuccess({ enabled: true }));
} else {
await pretty.success('Telemetry enabled');
}
break;
case 'disable':
disableTelemetry();
if (outputFormat === 'json') {
outputJson(formatSuccess({ enabled: false }));
} else {
await pretty.success('Telemetry disabled');
}
break;
case 'status':
await showStatus(outputFormat);
break;
case 'clear':
cleanupTelemetry();
if (outputFormat === 'json') {
outputJson(formatSuccess({ cleared: true }));
} else {
await pretty.success('Telemetry data cleared');
}
break;
case 'export':
await exportTelemetry(outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function showStatus(outputFormat: string): Promise<void> {
const telemetry = getTelemetry();
if (outputFormat === 'json') {
outputJson(formatSuccess({
enabled: telemetry.isEnabled()
}));
} else {
await pretty.heading('Telemetry Status');
await pretty.plain(`Enabled: ${telemetry.isEnabled() ? 'Yes' : 'No'}`);
}
}
async function exportTelemetry(outputFormat: string): Promise<void> {
const telemetry = getTelemetry();
if (outputFormat === 'json') {
outputJson(formatSuccess({
enabled: telemetry.isEnabled(),
exportedAt: new Date().toISOString()
}));
} else {
await pretty.plain(`Telemetry enabled: ${telemetry.isEnabled()}`);
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'telemetry',
subcommands: [
{ name: 'status', description: 'Show telemetry status' },
{ name: 'enable', description: 'Enable telemetry collection' },
{ name: 'disable', description: 'Disable telemetry collection' },
{ name: 'clear', description: 'Clear collected telemetry data' },
{ name: 'export', description: 'Export telemetry data' },
{ name: 'help', description: 'Show this help' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Telemetry Command');
await pretty.plain('Usage monitoring and analytics\n');
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(15)} ${cmd.description}`);
}
}
}
+160
View File
@@ -0,0 +1,160 @@
/**
* Tools command - List or manage tools
*/
import { getRegistry, FunctionTool } from '../../tools/decorator';
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface ToolsOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
interface ToolInfo {
name: string;
description?: string;
parameters?: unknown;
}
// Built-in tools that are always available
const BUILTIN_TOOLS: ToolInfo[] = [
{ name: 'arxiv_search', description: 'Search arXiv for academic papers' },
{ name: 'web_search', description: 'Search the web for information' },
{ name: 'calculator', description: 'Perform mathematical calculations' },
{ name: 'code_executor', description: 'Execute code snippets' },
{ name: 'file_reader', description: 'Read file contents' },
{ name: 'file_writer', description: 'Write content to files' }
];
export async function execute(args: string[], options: ToolsOptions): Promise<void> {
const subcommand = args[0] || 'list';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
switch (subcommand) {
case 'list':
await listTools(options, outputFormat);
break;
case 'info':
await toolInfo(args.slice(1), options, outputFormat);
break;
default:
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown subcommand: ${subcommand}`));
} else {
await pretty.error(`Unknown subcommand: ${subcommand}`);
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
}
async function listTools(options: ToolsOptions, outputFormat: string): Promise<void> {
// Get registered tools from registry
const registry = getRegistry();
const registeredTools = registry.list();
// Combine built-in and registered tools
const allTools: ToolInfo[] = [
...BUILTIN_TOOLS,
...registeredTools.map((t: FunctionTool) => ({
name: t.name,
description: t.description,
parameters: t.parameters
}))
];
if (outputFormat === 'json') {
outputJson(formatSuccess({
tools: allTools.map(t => ({
name: t.name,
description: t.description,
parameters: options.verbose ? t.parameters : undefined
})),
count: allTools.length
}));
} else {
await pretty.heading('Available Tools');
await pretty.plain('Built-in Tools:');
for (const tool of BUILTIN_TOOLS) {
await pretty.plain(` • ${tool.name}`);
if (tool.description) {
await pretty.dim(` ${tool.description}`);
}
}
if (registeredTools.length > 0) {
await pretty.newline();
await pretty.plain('Registered Tools:');
for (const tool of registeredTools) {
await pretty.plain(` • ${tool.name}`);
if (tool.description) {
await pretty.dim(` ${tool.description}`);
}
}
}
await pretty.newline();
await pretty.info(`Total: ${allTools.length} tools`);
}
}
async function toolInfo(args: string[], options: ToolsOptions, outputFormat: string): Promise<void> {
const toolName = args[0];
if (!toolName) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a tool name'));
} else {
await pretty.error('Please provide a tool name');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
// Look for tool in built-in and registered tools
const builtinTool = BUILTIN_TOOLS.find(t => t.name === toolName);
const registry = getRegistry();
const registeredTool = registry.get(toolName);
const tool = builtinTool || (registeredTool ? {
name: registeredTool.name,
description: registeredTool.description,
parameters: registeredTool.parameters
} : null);
if (!tool) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.TOOL_NOT_FOUND, `Tool not found: ${toolName}`));
} else {
await pretty.error(`Tool not found: ${toolName}`);
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
if (outputFormat === 'json') {
outputJson(formatSuccess({
tool: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
type: builtinTool ? 'builtin' : 'registered'
}
}));
} else {
await pretty.heading(`Tool: ${tool.name}`);
await pretty.keyValue({
'Name': tool.name,
'Type': builtinTool ? 'Built-in' : 'Registered',
'Description': tool.description || 'No description'
});
if (tool.parameters && options.verbose) {
await pretty.newline();
await pretty.plain('Parameters:');
await pretty.plain(JSON.stringify(tool.parameters, null, 2));
}
}
}
+146
View File
@@ -0,0 +1,146 @@
/**
* Vector command - Vector store management
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface VectorOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
store?: string;
}
export async function execute(args: string[], options: VectorOptions): Promise<void> {
const action = args[0] || 'help';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'providers':
await listProviders(outputFormat);
break;
case 'info':
await showInfo(outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function showInfo(outputFormat: string): Promise<void> {
const info = {
feature: 'Vector Stores',
description: 'Vector database integrations for embeddings and similarity search',
providers: [
{ name: 'MemoryVectorStore', description: 'In-memory vector store for development' },
{ name: 'PineconeVectorStore', description: 'Pinecone cloud vector database' },
{ name: 'WeaviateVectorStore', description: 'Weaviate vector search engine' },
{ name: 'QdrantVectorStore', description: 'Qdrant vector database' },
{ name: 'ChromaVectorStore', description: 'ChromaDB embedding database' }
],
sdkUsage: `
import { createMemoryVectorStore, createPineconeStore } from 'praisonai';
// Create a memory vector store
const store = createMemoryVectorStore('my-store');
// Create index
await store.createIndex({ indexName: 'docs', dimension: 1536 });
// Upsert vectors
await store.upsert({
indexName: 'docs',
vectors: [{ id: '1', vector: [...], metadata: { text: 'Hello' } }]
});
// Query
const results = await store.query({
indexName: 'docs',
vector: [...],
topK: 5
});
`
};
if (outputFormat === 'json') {
outputJson(formatSuccess(info));
} else {
await pretty.heading('Vector Stores');
await pretty.plain(info.description);
await pretty.newline();
await pretty.plain('Available Providers:');
for (const p of info.providers) {
await pretty.plain(` • ${p.name}: ${p.description}`);
}
await pretty.newline();
await pretty.dim('Use the SDK for full vector store functionality');
}
}
async function listProviders(outputFormat: string): Promise<void> {
const providers = [
{ name: 'memory', description: 'In-memory vector store (default)', available: true },
{ name: 'pinecone', description: 'Pinecone vector database', available: true },
{ name: 'weaviate', description: 'Weaviate vector database', available: true },
{ name: 'qdrant', description: 'Qdrant vector database', available: true },
{ name: 'chroma', description: 'ChromaDB vector database', available: true }
];
if (outputFormat === 'json') {
outputJson(formatSuccess({ providers }));
} else {
await pretty.heading('Vector Store Providers');
for (const p of providers) {
const status = p.available ? '✓' : '✗';
await pretty.plain(` ${status} ${p.name.padEnd(15)} ${p.description}`);
}
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'vector',
description: 'Vector store management for embeddings and similarity search',
subcommands: [
{ name: 'add <content>', description: 'Add document to vector store' },
{ name: 'search <query>', description: 'Search vector store' },
{ name: 'stats', description: 'Show vector store statistics' },
{ name: 'providers', description: 'List available vector store providers' },
{ name: 'help', description: 'Show this help' }
],
flags: [
{ name: '--store', description: 'Vector store provider (memory, pinecone, etc.)' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Vector Command');
await pretty.plain(help.description);
await pretty.newline();
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(25)} ${cmd.description}`);
}
await pretty.newline();
await pretty.plain('Flags:');
for (const flag of help.flags) {
await pretty.plain(` ${flag.name.padEnd(20)} ${flag.description}`);
}
}
}
@@ -0,0 +1,40 @@
/**
* Version command - Show CLI version
*/
import { CLI_SPEC_VERSION } from '../spec/cli-spec';
import { outputJson, formatSuccess } from '../output/json';
import * as pretty from '../output/pretty';
export interface VersionOptions {
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: VersionOptions): Promise<void> {
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
// Load package.json for version
let packageVersion = '0.0.0';
try {
const pkg = require('../../../package.json');
packageVersion = pkg.version;
} catch {
// Fallback if package.json not found
}
const versionInfo = {
version: packageVersion,
cli_spec: CLI_SPEC_VERSION,
runtime: 'node',
node_version: process.version
};
if (outputFormat === 'json') {
outputJson(formatSuccess(versionInfo));
} else {
await pretty.plain(`praisonai-ts v${packageVersion}`);
await pretty.dim(`CLI Spec: v${CLI_SPEC_VERSION}`);
await pretty.dim(`Node: ${process.version}`);
}
}
+137
View File
@@ -0,0 +1,137 @@
/**
* Voice command - Text-to-speech and speech-to-text
*/
import { outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES } from '../output/errors';
export interface VoiceOptions {
verbose?: boolean;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
provider?: string;
}
export async function execute(args: string[], options: VoiceOptions): Promise<void> {
const action = args[0] || 'help';
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
switch (action) {
case 'providers':
await listProviders(outputFormat);
break;
case 'info':
await showInfo(outputFormat);
break;
case 'help':
default:
await showHelp(outputFormat);
break;
}
} catch (error) {
if (outputFormat === 'json') {
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
} else {
await pretty.error(error instanceof Error ? error.message : String(error));
}
process.exit(EXIT_CODES.RUNTIME_ERROR);
}
}
async function showInfo(outputFormat: string): Promise<void> {
const info = {
feature: 'Voice',
description: 'Text-to-speech and speech-to-text capabilities',
providers: [
{ name: 'OpenAIVoiceProvider', description: 'OpenAI TTS and Whisper' },
{ name: 'ElevenLabsVoiceProvider', description: 'ElevenLabs voice synthesis' }
],
capabilities: [
'Text-to-speech conversion',
'Speech-to-text transcription',
'Multiple voice options',
'Streaming audio support'
],
sdkUsage: `
import { createOpenAIVoice, createElevenLabsVoice } from 'praisonai';
// Create OpenAI voice provider
const voice = createOpenAIVoice({ apiKey: process.env.OPENAI_API_KEY });
// Text to speech
const audio = await voice.speak({ text: 'Hello world', voice: 'alloy' });
// Speech to text
const text = await voice.listen({ audio: audioBuffer });
`
};
if (outputFormat === 'json') {
outputJson(formatSuccess(info));
} else {
await pretty.heading('Voice');
await pretty.plain(info.description);
await pretty.newline();
await pretty.plain('Providers:');
for (const p of info.providers) {
await pretty.plain(` • ${p.name}: ${p.description}`);
}
await pretty.newline();
await pretty.plain('Capabilities:');
for (const cap of info.capabilities) {
await pretty.plain(` • ${cap}`);
}
}
}
async function listProviders(outputFormat: string): Promise<void> {
const providers = [
{ name: 'openai', description: 'OpenAI TTS and Whisper', available: true },
{ name: 'elevenlabs', description: 'ElevenLabs voice synthesis', available: true }
];
if (outputFormat === 'json') {
outputJson(formatSuccess({ providers }));
} else {
await pretty.heading('Voice Providers');
for (const p of providers) {
const status = p.available ? '✓' : '✗';
await pretty.plain(` ${status} ${p.name.padEnd(15)} ${p.description}`);
}
}
}
async function showHelp(outputFormat: string): Promise<void> {
const help = {
command: 'voice',
description: 'Text-to-speech and speech-to-text capabilities',
subcommands: [
{ name: 'info', description: 'Show voice feature information' },
{ name: 'providers', description: 'List available voice providers' },
{ name: 'help', description: 'Show this help' }
],
flags: [
{ name: '--provider', description: 'Voice provider (openai, elevenlabs)' }
]
};
if (outputFormat === 'json') {
outputJson(formatSuccess(help));
} else {
await pretty.heading('Voice Command');
await pretty.plain(help.description);
await pretty.newline();
await pretty.plain('Subcommands:');
for (const cmd of help.subcommands) {
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
}
await pretty.newline();
await pretty.plain('Flags:');
for (const flag of help.flags) {
await pretty.plain(` ${flag.name.padEnd(20)} ${flag.description}`);
}
}
}
@@ -0,0 +1,188 @@
/**
* Workflow command - Execute a multi-agent workflow
*/
import * as fs from 'fs';
import * as path from 'path';
import { resolveConfig } from '../config/resolve';
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES, normalizeError } from '../output/errors';
export interface WorkflowOptions {
parallel?: boolean;
model?: string;
verbose?: boolean;
profile?: string;
config?: string;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
interface WorkflowStep {
name: string;
agent?: string;
task?: string;
depends_on?: string[];
}
interface WorkflowDefinition {
name?: string;
description?: string;
agents?: Record<string, any>;
steps?: WorkflowStep[];
}
/**
* Simple YAML parser for workflow files
*/
function parseWorkflowYaml(content: string): WorkflowDefinition {
const result: WorkflowDefinition = {};
const lines = content.split('\n');
let currentSection: string | null = null;
let currentItem: any = null;
let currentKey: string | null = null;
for (const line of lines) {
if (!line.trim() || line.trim().startsWith('#')) continue;
const indent = line.search(/\S/);
const trimmed = line.trim();
// Top-level keys
if (indent === 0 && trimmed.includes(':')) {
const colonIdx = trimmed.indexOf(':');
const key = trimmed.slice(0, colonIdx).trim();
const value = trimmed.slice(colonIdx + 1).trim();
if (value) {
(result as any)[key] = value;
} else {
currentSection = key;
if (key === 'agents') result.agents = {};
if (key === 'steps') result.steps = [];
}
} else if (indent > 0 && currentSection) {
// Handle list items (steps)
if (trimmed.startsWith('- ')) {
if (currentSection === 'steps') {
currentItem = {};
result.steps!.push(currentItem);
// Parse inline values after -
const afterDash = trimmed.slice(2).trim();
if (afterDash.includes(':')) {
const colonIdx = afterDash.indexOf(':');
const key = afterDash.slice(0, colonIdx).trim();
const value = afterDash.slice(colonIdx + 1).trim();
currentItem[key] = value;
}
}
} else if (trimmed.includes(':') && currentItem) {
const colonIdx = trimmed.indexOf(':');
const key = trimmed.slice(0, colonIdx).trim();
const value = trimmed.slice(colonIdx + 1).trim();
currentItem[key] = value || [];
}
}
}
return result;
}
export async function execute(args: string[], options: WorkflowOptions): Promise<void> {
const workflowFile = args[0];
if (!workflowFile) {
if (options.json || options.output === 'json') {
printError(ERROR_CODES.MISSING_ARG, 'Please provide a workflow file path');
} else {
await pretty.error('Please provide a workflow file path');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
// Resolve config
const config = resolveConfig({
configPath: options.config,
profile: options.profile,
model: options.model,
verbose: options.verbose
});
const startTime = Date.now();
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
// Read and parse workflow file
const filePath = path.resolve(workflowFile);
if (!fs.existsSync(filePath)) {
throw new Error(`Workflow file not found: ${filePath}`);
}
const content = fs.readFileSync(filePath, 'utf-8');
const workflow = parseWorkflowYaml(content);
if (outputFormat !== 'json') {
await pretty.heading(`Executing workflow: ${workflow.name || 'Unnamed'}`);
if (workflow.description) {
await pretty.info(workflow.description);
}
}
// Execute workflow steps
const results: Record<string, any> = {};
const steps = workflow.steps || [];
for (const step of steps) {
if (outputFormat !== 'json') {
await pretty.info(`Running step: ${step.name}`);
}
// For now, we'll simulate step execution
// In a full implementation, this would create agents and execute tasks
results[step.name] = {
status: 'completed',
output: `Step ${step.name} completed`
};
}
const duration = Date.now() - startTime;
if (outputFormat === 'json') {
outputJson(formatSuccess(
{
workflow: workflow.name || 'Unnamed',
steps: steps.length,
results
},
{
duration_ms: duration,
model: config.model
}
));
} else {
await pretty.success(`Workflow completed in ${duration}ms`);
await pretty.newline();
await pretty.keyValue({
'Steps executed': steps.length,
'Duration': `${duration}ms`
});
}
} catch (error) {
const cliError = normalizeError(error);
if (outputFormat === 'json') {
outputJson(formatError(cliError.code, cliError.message, cliError.details));
} else {
await pretty.error(cliError.message);
if (config.verbose && error instanceof Error && error.stack) {
await pretty.dim(error.stack);
}
}
process.exit(cliError.exitCode);
}
}
@@ -0,0 +1,297 @@
/**
* Autonomy Modes - Control automatic execution behavior
*/
export type AutonomyMode = 'suggest' | 'auto_edit' | 'full_auto';
export type ActionType =
| 'file_read'
| 'file_write'
| 'file_delete'
| 'shell_command'
| 'network_request'
| 'git_operation'
| 'install_package';
export interface ApprovalPolicy {
action: ActionType;
requiresApproval: boolean;
autoApprovePatterns?: string[];
autoDenyPatterns?: string[];
}
export interface AutonomyConfig {
mode: AutonomyMode;
policies?: ApprovalPolicy[];
rememberDecisions?: boolean;
maxAutoActions?: number;
}
export interface ActionRequest {
type: ActionType;
description: string;
target?: string;
details?: Record<string, any>;
}
export interface ActionDecision {
approved: boolean;
reason?: string;
remembered?: boolean;
}
/**
* Default policies for each mode
*/
export const MODE_POLICIES: Record<AutonomyMode, ApprovalPolicy[]> = {
suggest: [
{ action: 'file_read', requiresApproval: false },
{ action: 'file_write', requiresApproval: true },
{ action: 'file_delete', requiresApproval: true },
{ action: 'shell_command', requiresApproval: true },
{ action: 'network_request', requiresApproval: true },
{ action: 'git_operation', requiresApproval: true },
{ action: 'install_package', requiresApproval: true }
],
auto_edit: [
{ action: 'file_read', requiresApproval: false },
{ action: 'file_write', requiresApproval: false },
{ action: 'file_delete', requiresApproval: true },
{ action: 'shell_command', requiresApproval: true },
{ action: 'network_request', requiresApproval: false },
{ action: 'git_operation', requiresApproval: true },
{ action: 'install_package', requiresApproval: true }
],
full_auto: [
{ action: 'file_read', requiresApproval: false },
{ action: 'file_write', requiresApproval: false },
{ action: 'file_delete', requiresApproval: false },
{ action: 'shell_command', requiresApproval: false },
{ action: 'network_request', requiresApproval: false },
{ action: 'git_operation', requiresApproval: false },
{ action: 'install_package', requiresApproval: true }
]
};
/**
* Autonomy Manager class
*/
export class AutonomyManager {
private config: AutonomyConfig;
private rememberedDecisions: Map<string, boolean> = new Map();
private actionCount: number = 0;
private promptCallback?: (request: ActionRequest) => Promise<boolean>;
constructor(config: Partial<AutonomyConfig> = {}) {
this.config = {
mode: 'suggest',
rememberDecisions: true,
maxAutoActions: 100,
...config
};
}
/**
* Set the prompt callback for user approval
*/
setPromptCallback(callback: (request: ActionRequest) => Promise<boolean>): void {
this.promptCallback = callback;
}
/**
* Request approval for an action
*/
async requestApproval(request: ActionRequest): Promise<ActionDecision> {
// Check if we've exceeded max auto actions
if (this.config.maxAutoActions && this.actionCount >= this.config.maxAutoActions) {
return {
approved: false,
reason: 'Maximum auto actions exceeded'
};
}
// Check remembered decisions
const decisionKey = this.getDecisionKey(request);
if (this.config.rememberDecisions && this.rememberedDecisions.has(decisionKey)) {
const remembered = this.rememberedDecisions.get(decisionKey)!;
if (remembered) this.actionCount++;
return {
approved: remembered,
reason: 'Remembered decision',
remembered: true
};
}
// Get policy for this action type
const policy = this.getPolicy(request.type);
// Check auto-approve patterns
if (policy.autoApprovePatterns && request.target) {
for (const pattern of policy.autoApprovePatterns) {
if (this.matchPattern(request.target, pattern)) {
this.actionCount++;
return { approved: true, reason: `Matched auto-approve pattern: ${pattern}` };
}
}
}
// Check auto-deny patterns
if (policy.autoDenyPatterns && request.target) {
for (const pattern of policy.autoDenyPatterns) {
if (this.matchPattern(request.target, pattern)) {
return { approved: false, reason: `Matched auto-deny pattern: ${pattern}` };
}
}
}
// If no approval required, auto-approve
if (!policy.requiresApproval) {
this.actionCount++;
return { approved: true, reason: 'No approval required' };
}
// Prompt user for approval
if (this.promptCallback) {
const approved = await this.promptCallback(request);
// Remember decision if enabled
if (this.config.rememberDecisions) {
this.rememberedDecisions.set(decisionKey, approved);
}
if (approved) this.actionCount++;
return { approved, reason: 'User decision' };
}
// No prompt callback, deny by default
return { approved: false, reason: 'No approval mechanism available' };
}
/**
* Get policy for an action type
*/
private getPolicy(actionType: ActionType): ApprovalPolicy {
// Check custom policies first
if (this.config.policies) {
const custom = this.config.policies.find(p => p.action === actionType);
if (custom) return custom;
}
// Fall back to mode defaults
const modePolicy = MODE_POLICIES[this.config.mode].find(p => p.action === actionType);
return modePolicy || { action: actionType, requiresApproval: true };
}
/**
* Generate decision key for remembering
*/
private getDecisionKey(request: ActionRequest): string {
return `${request.type}:${request.target || 'default'}`;
}
/**
* Match a target against a pattern
*/
private matchPattern(target: string, pattern: string): boolean {
// Simple glob-like matching
const regex = new RegExp(
'^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
);
return regex.test(target);
}
/**
* Get current mode
*/
getMode(): AutonomyMode {
return this.config.mode;
}
/**
* Set mode
*/
setMode(mode: AutonomyMode): void {
this.config.mode = mode;
}
/**
* Add custom policy
*/
addPolicy(policy: ApprovalPolicy): void {
if (!this.config.policies) {
this.config.policies = [];
}
// Remove existing policy for this action
this.config.policies = this.config.policies.filter(p => p.action !== policy.action);
this.config.policies.push(policy);
}
/**
* Clear remembered decisions
*/
clearRemembered(): void {
this.rememberedDecisions.clear();
}
/**
* Reset action count
*/
resetActionCount(): void {
this.actionCount = 0;
}
/**
* Get action count
*/
getActionCount(): number {
return this.actionCount;
}
/**
* Get summary of current settings
*/
getSummary(): string {
const lines = [
`Autonomy Mode: ${this.config.mode}`,
`Actions taken: ${this.actionCount}/${this.config.maxAutoActions || 'unlimited'}`,
`Remembered decisions: ${this.rememberedDecisions.size}`,
'',
'Current policies:'
];
const policies = MODE_POLICIES[this.config.mode];
for (const policy of policies) {
const status = policy.requiresApproval ? '⚠️ requires approval' : '✓ auto-approved';
lines.push(` ${policy.action}: ${status}`);
}
return lines.join('\n');
}
}
/**
* Create an autonomy manager
*/
export function createAutonomyManager(config?: Partial<AutonomyConfig>): AutonomyManager {
return new AutonomyManager(config);
}
/**
* Simple approval prompt for CLI
*/
export async function cliApprovalPrompt(request: ActionRequest): Promise<boolean> {
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
const message = `\n⚠️ Action requires approval:\n Type: ${request.type}\n ${request.description}\n Target: ${request.target || 'N/A'}\n\nApprove? (y/n): `;
rl.question(message, (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
});
});
}
@@ -0,0 +1,466 @@
/**
* Background Jobs - Persisted job queue with adapter support
*/
export type JobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
export type JobPriority = 'low' | 'normal' | 'high' | 'critical';
export interface Job<T = any> {
id: string;
name: string;
data: T;
status: JobStatus;
priority: JobPriority;
attempts: number;
maxAttempts: number;
createdAt: Date;
startedAt?: Date;
completedAt?: Date;
result?: any;
error?: string;
progress?: number;
metadata?: Record<string, any>;
}
export interface JobQueueConfig {
name?: string;
concurrency?: number;
maxAttempts?: number;
retryDelay?: number;
storage?: JobStorageAdapter;
}
export interface JobStorageAdapter {
save(job: Job): Promise<void>;
get(id: string): Promise<Job | null>;
getAll(): Promise<Job[]>;
getByStatus(status: JobStatus): Promise<Job[]>;
update(id: string, updates: Partial<Job>): Promise<void>;
delete(id: string): Promise<void>;
clear(): Promise<void>;
}
export type JobHandler<T = any, R = any> = (job: Job<T>, context: JobContext) => Promise<R>;
export interface JobContext {
updateProgress: (progress: number) => Promise<void>;
log: (message: string) => void;
}
/**
* In-memory job storage adapter
*/
export class MemoryJobStorage implements JobStorageAdapter {
private jobs: Map<string, Job> = new Map();
async save(job: Job): Promise<void> {
this.jobs.set(job.id, { ...job });
}
async get(id: string): Promise<Job | null> {
return this.jobs.get(id) || null;
}
async getAll(): Promise<Job[]> {
return Array.from(this.jobs.values());
}
async getByStatus(status: JobStatus): Promise<Job[]> {
return Array.from(this.jobs.values()).filter(j => j.status === status);
}
async update(id: string, updates: Partial<Job>): Promise<void> {
const job = this.jobs.get(id);
if (job) {
this.jobs.set(id, { ...job, ...updates });
}
}
async delete(id: string): Promise<void> {
this.jobs.delete(id);
}
async clear(): Promise<void> {
this.jobs.clear();
}
}
/**
* File-based job storage adapter
*/
export class FileJobStorage implements JobStorageAdapter {
private filePath: string;
private jobs: Map<string, Job> = new Map();
private initialized: boolean = false;
constructor(filePath: string) {
this.filePath = filePath;
}
private async init(): Promise<void> {
if (this.initialized) return;
try {
const fs = await import('fs/promises');
const content = await fs.readFile(this.filePath, 'utf-8');
const data = JSON.parse(content);
for (const job of data) {
job.createdAt = new Date(job.createdAt);
if (job.startedAt) job.startedAt = new Date(job.startedAt);
if (job.completedAt) job.completedAt = new Date(job.completedAt);
this.jobs.set(job.id, job);
}
} catch {
// File doesn't exist yet
}
this.initialized = true;
}
private async persist(): Promise<void> {
const fs = await import('fs/promises');
const data = Array.from(this.jobs.values());
await fs.writeFile(this.filePath, JSON.stringify(data, null, 2));
}
async save(job: Job): Promise<void> {
await this.init();
this.jobs.set(job.id, { ...job });
await this.persist();
}
async get(id: string): Promise<Job | null> {
await this.init();
return this.jobs.get(id) || null;
}
async getAll(): Promise<Job[]> {
await this.init();
return Array.from(this.jobs.values());
}
async getByStatus(status: JobStatus): Promise<Job[]> {
await this.init();
return Array.from(this.jobs.values()).filter(j => j.status === status);
}
async update(id: string, updates: Partial<Job>): Promise<void> {
await this.init();
const job = this.jobs.get(id);
if (job) {
this.jobs.set(id, { ...job, ...updates });
await this.persist();
}
}
async delete(id: string): Promise<void> {
await this.init();
this.jobs.delete(id);
await this.persist();
}
async clear(): Promise<void> {
this.jobs.clear();
await this.persist();
}
}
/**
* Job Queue class
*/
export class JobQueue {
private config: JobQueueConfig;
private storage: JobStorageAdapter;
private handlers: Map<string, JobHandler> = new Map();
private running: boolean = false;
private activeJobs: number = 0;
private processInterval?: NodeJS.Timeout;
constructor(config: JobQueueConfig = {}) {
this.config = {
name: 'default',
concurrency: 1,
maxAttempts: 3,
retryDelay: 1000,
...config
};
this.storage = config.storage || new MemoryJobStorage();
}
/**
* Register a job handler
*/
register<T = any, R = any>(name: string, handler: JobHandler<T, R>): void {
this.handlers.set(name, handler);
}
/**
* Add a job to the queue
*/
async add<T = any>(name: string, data: T, options: Partial<Job<T>> = {}): Promise<Job<T>> {
const job: Job<T> = {
id: `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name,
data,
status: 'pending',
priority: options.priority || 'normal',
attempts: 0,
maxAttempts: options.maxAttempts || this.config.maxAttempts || 3,
createdAt: new Date(),
metadata: options.metadata
};
await this.storage.save(job);
return job;
}
/**
* Get a job by ID
*/
async get(id: string): Promise<Job | null> {
return this.storage.get(id);
}
/**
* Get all jobs
*/
async getAll(): Promise<Job[]> {
return this.storage.getAll();
}
/**
* Get jobs by status
*/
async getByStatus(status: JobStatus): Promise<Job[]> {
return this.storage.getByStatus(status);
}
/**
* Cancel a job
*/
async cancel(id: string): Promise<boolean> {
const job = await this.storage.get(id);
if (!job || job.status === 'running' || job.status === 'completed') {
return false;
}
await this.storage.update(id, { status: 'cancelled' });
return true;
}
/**
* Retry a failed job
*/
async retry(id: string): Promise<boolean> {
const job = await this.storage.get(id);
if (!job || job.status !== 'failed') {
return false;
}
await this.storage.update(id, {
status: 'pending',
attempts: 0,
error: undefined
});
return true;
}
/**
* Start processing jobs
*/
start(): void {
if (this.running) return;
this.running = true;
this.processInterval = setInterval(() => this.process(), 100);
}
/**
* Stop processing jobs
*/
stop(): void {
this.running = false;
if (this.processInterval) {
clearInterval(this.processInterval);
this.processInterval = undefined;
}
}
/**
* Process pending jobs
*/
private async process(): Promise<void> {
if (!this.running) return;
if (this.activeJobs >= (this.config.concurrency || 1)) return;
const pendingJobs = await this.storage.getByStatus('pending');
// Sort by priority and creation time
const priorityOrder: Record<JobPriority, number> = {
critical: 0,
high: 1,
normal: 2,
low: 3
};
pendingJobs.sort((a, b) => {
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
if (priorityDiff !== 0) return priorityDiff;
return a.createdAt.getTime() - b.createdAt.getTime();
});
for (const job of pendingJobs) {
if (this.activeJobs >= (this.config.concurrency || 1)) break;
this.runJob(job);
}
}
/**
* Run a single job
*/
private async runJob(job: Job): Promise<void> {
const handler = this.handlers.get(job.name);
if (!handler) {
await this.storage.update(job.id, {
status: 'failed',
error: `No handler registered for job type: ${job.name}`
});
return;
}
this.activeJobs++;
await this.storage.update(job.id, {
status: 'running',
startedAt: new Date(),
attempts: job.attempts + 1
});
const context: JobContext = {
updateProgress: async (progress: number) => {
await this.storage.update(job.id, { progress });
},
log: (message: string) => {
console.log(`[Job ${job.id}] ${message}`);
}
};
try {
const result = await handler(job, context);
await this.storage.update(job.id, {
status: 'completed',
completedAt: new Date(),
result,
progress: 100
});
} catch (error: any) {
const attempts = job.attempts + 1;
if (attempts < job.maxAttempts) {
// Retry after delay
setTimeout(async () => {
await this.storage.update(job.id, {
status: 'pending',
error: error.message
});
}, this.config.retryDelay || 1000);
} else {
await this.storage.update(job.id, {
status: 'failed',
completedAt: new Date(),
error: error.message
});
}
} finally {
this.activeJobs--;
}
}
/**
* Process a job immediately (bypass queue)
*/
async processNow(id: string): Promise<any> {
const job = await this.storage.get(id);
if (!job) throw new Error(`Job ${id} not found`);
const handler = this.handlers.get(job.name);
if (!handler) throw new Error(`No handler for job type: ${job.name}`);
await this.storage.update(job.id, {
status: 'running',
startedAt: new Date()
});
const context: JobContext = {
updateProgress: async (progress: number) => {
await this.storage.update(job.id, { progress });
},
log: console.log
};
try {
const result = await handler(job, context);
await this.storage.update(job.id, {
status: 'completed',
completedAt: new Date(),
result
});
return result;
} catch (error: any) {
await this.storage.update(job.id, {
status: 'failed',
completedAt: new Date(),
error: error.message
});
throw error;
}
}
/**
* Get queue stats
*/
async getStats(): Promise<{
pending: number;
running: number;
completed: number;
failed: number;
cancelled: number;
}> {
const all = await this.storage.getAll();
return {
pending: all.filter(j => j.status === 'pending').length,
running: all.filter(j => j.status === 'running').length,
completed: all.filter(j => j.status === 'completed').length,
failed: all.filter(j => j.status === 'failed').length,
cancelled: all.filter(j => j.status === 'cancelled').length
};
}
/**
* Clear completed/failed jobs
*/
async cleanup(olderThan?: Date): Promise<number> {
const all = await this.storage.getAll();
let count = 0;
for (const job of all) {
if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
if (!olderThan || (job.completedAt && job.completedAt < olderThan)) {
await this.storage.delete(job.id);
count++;
}
}
}
return count;
}
}
/**
* Create a job queue
*/
export function createJobQueue(config?: JobQueueConfig): JobQueue {
return new JobQueue(config);
}
/**
* Create a file-based job storage
*/
export function createFileJobStorage(filePath: string): FileJobStorage {
return new FileJobStorage(filePath);
}
@@ -0,0 +1,310 @@
/**
* Checkpoints - Snapshot session/run state for recovery
*/
export interface CheckpointData {
id: string;
name: string;
timestamp: Date;
sessionId?: string;
runId?: string;
agentId?: string;
state: Record<string, any>;
messages?: any[];
metadata?: Record<string, any>;
}
export interface CheckpointConfig {
storage?: CheckpointStorage;
autoSave?: boolean;
autoSaveInterval?: number;
maxCheckpoints?: number;
}
export interface CheckpointStorage {
save(checkpoint: CheckpointData): Promise<void>;
load(id: string): Promise<CheckpointData | null>;
list(): Promise<CheckpointData[]>;
delete(id: string): Promise<void>;
clear(): Promise<void>;
}
/**
* In-memory checkpoint storage
*/
export class MemoryCheckpointStorage implements CheckpointStorage {
private checkpoints: Map<string, CheckpointData> = new Map();
async save(checkpoint: CheckpointData): Promise<void> {
this.checkpoints.set(checkpoint.id, { ...checkpoint });
}
async load(id: string): Promise<CheckpointData | null> {
return this.checkpoints.get(id) || null;
}
async list(): Promise<CheckpointData[]> {
return Array.from(this.checkpoints.values())
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
async delete(id: string): Promise<void> {
this.checkpoints.delete(id);
}
async clear(): Promise<void> {
this.checkpoints.clear();
}
}
/**
* File-based checkpoint storage
*/
export class FileCheckpointStorage implements CheckpointStorage {
private dirPath: string;
constructor(dirPath: string) {
this.dirPath = dirPath;
}
private async ensureDir(): Promise<void> {
const fs = await import('fs/promises');
await fs.mkdir(this.dirPath, { recursive: true });
}
private getFilePath(id: string): string {
return `${this.dirPath}/${id}.json`;
}
async save(checkpoint: CheckpointData): Promise<void> {
await this.ensureDir();
const fs = await import('fs/promises');
await fs.writeFile(
this.getFilePath(checkpoint.id),
JSON.stringify(checkpoint, null, 2)
);
}
async load(id: string): Promise<CheckpointData | null> {
try {
const fs = await import('fs/promises');
const content = await fs.readFile(this.getFilePath(id), 'utf-8');
const data = JSON.parse(content);
data.timestamp = new Date(data.timestamp);
return data;
} catch {
return null;
}
}
async list(): Promise<CheckpointData[]> {
try {
await this.ensureDir();
const fs = await import('fs/promises');
const files = await fs.readdir(this.dirPath);
const checkpoints: CheckpointData[] = [];
for (const file of files) {
if (file.endsWith('.json')) {
const id = file.replace('.json', '');
const checkpoint = await this.load(id);
if (checkpoint) {
checkpoints.push(checkpoint);
}
}
}
return checkpoints.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
} catch {
return [];
}
}
async delete(id: string): Promise<void> {
try {
const fs = await import('fs/promises');
await fs.unlink(this.getFilePath(id));
} catch {
// Ignore if file doesn't exist
}
}
async clear(): Promise<void> {
const checkpoints = await this.list();
for (const checkpoint of checkpoints) {
await this.delete(checkpoint.id);
}
}
}
/**
* Checkpoint Manager class
*/
export class CheckpointManager {
private config: CheckpointConfig;
private storage: CheckpointStorage;
private autoSaveTimer?: NodeJS.Timeout;
private currentState: Record<string, any> = {};
constructor(config: CheckpointConfig = {}) {
this.config = {
autoSave: false,
autoSaveInterval: 60000, // 1 minute
maxCheckpoints: 10,
...config
};
this.storage = config.storage || new MemoryCheckpointStorage();
}
/**
* Create a checkpoint
*/
async create(name: string, options: Partial<CheckpointData> = {}): Promise<CheckpointData> {
const checkpoint: CheckpointData = {
id: `cp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name,
timestamp: new Date(),
state: { ...this.currentState },
...options
};
await this.storage.save(checkpoint);
await this.enforceMaxCheckpoints();
return checkpoint;
}
/**
* Restore from a checkpoint
*/
async restore(id: string): Promise<CheckpointData | null> {
const checkpoint = await this.storage.load(id);
if (checkpoint) {
this.currentState = { ...checkpoint.state };
}
return checkpoint;
}
/**
* Restore the latest checkpoint
*/
async restoreLatest(): Promise<CheckpointData | null> {
const checkpoints = await this.storage.list();
if (checkpoints.length === 0) return null;
return this.restore(checkpoints[0].id);
}
/**
* Get a checkpoint by ID
*/
async get(id: string): Promise<CheckpointData | null> {
return this.storage.load(id);
}
/**
* List all checkpoints
*/
async list(): Promise<CheckpointData[]> {
return this.storage.list();
}
/**
* Delete a checkpoint
*/
async delete(id: string): Promise<void> {
await this.storage.delete(id);
}
/**
* Clear all checkpoints
*/
async clear(): Promise<void> {
await this.storage.clear();
}
/**
* Update current state
*/
setState(state: Record<string, any>): void {
this.currentState = { ...this.currentState, ...state };
}
/**
* Get current state
*/
getState(): Record<string, any> {
return { ...this.currentState };
}
/**
* Start auto-save
*/
startAutoSave(): void {
if (this.autoSaveTimer) return;
if (!this.config.autoSave) return;
this.autoSaveTimer = setInterval(async () => {
await this.create('auto-save', { metadata: { auto: true } });
}, this.config.autoSaveInterval);
}
/**
* Stop auto-save
*/
stopAutoSave(): void {
if (this.autoSaveTimer) {
clearInterval(this.autoSaveTimer);
this.autoSaveTimer = undefined;
}
}
/**
* Enforce maximum checkpoints limit
*/
private async enforceMaxCheckpoints(): Promise<void> {
if (!this.config.maxCheckpoints) return;
const checkpoints = await this.storage.list();
if (checkpoints.length > this.config.maxCheckpoints) {
// Delete oldest checkpoints (but keep auto-saves separate)
const toDelete = checkpoints.slice(this.config.maxCheckpoints);
for (const checkpoint of toDelete) {
await this.storage.delete(checkpoint.id);
}
}
}
/**
* Export checkpoint to JSON string
*/
async export(id: string): Promise<string | null> {
const checkpoint = await this.storage.load(id);
if (!checkpoint) return null;
return JSON.stringify(checkpoint, null, 2);
}
/**
* Import checkpoint from JSON string
*/
async import(json: string): Promise<CheckpointData> {
const data = JSON.parse(json);
data.timestamp = new Date(data.timestamp);
data.id = `cp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // New ID
await this.storage.save(data);
return data;
}
}
/**
* Create a checkpoint manager
*/
export function createCheckpointManager(config?: CheckpointConfig): CheckpointManager {
return new CheckpointManager(config);
}
/**
* Create file-based checkpoint storage
*/
export function createFileCheckpointStorage(dirPath: string): FileCheckpointStorage {
return new FileCheckpointStorage(dirPath);
}
@@ -0,0 +1,275 @@
/**
* Cost Tracker - Track token usage and costs across LLM requests
*/
export interface ModelPricing {
inputCostPer1k: number;
outputCostPer1k: number;
}
export interface TokenUsage {
inputTokens: number;
outputTokens: number;
totalTokens: number;
}
export interface RequestStats {
timestamp: number;
model: string;
usage: TokenUsage;
cost: number;
latencyMs?: number;
}
export interface SessionStats {
totalInputTokens: number;
totalOutputTokens: number;
totalTokens: number;
totalCost: number;
requestCount: number;
avgLatencyMs: number;
startTime: number;
requests: RequestStats[];
}
/**
* Model pricing data (USD per 1K tokens)
*/
export const MODEL_PRICING: Record<string, ModelPricing> = {
// OpenAI
'gpt-4o': { inputCostPer1k: 0.005, outputCostPer1k: 0.015 },
'gpt-4o-mini': { inputCostPer1k: 0.00015, outputCostPer1k: 0.0006 },
'gpt-4-turbo': { inputCostPer1k: 0.01, outputCostPer1k: 0.03 },
'gpt-4': { inputCostPer1k: 0.03, outputCostPer1k: 0.06 },
'gpt-3.5-turbo': { inputCostPer1k: 0.0005, outputCostPer1k: 0.0015 },
'o1': { inputCostPer1k: 0.015, outputCostPer1k: 0.06 },
'o1-mini': { inputCostPer1k: 0.003, outputCostPer1k: 0.012 },
'o1-preview': { inputCostPer1k: 0.015, outputCostPer1k: 0.06 },
// Anthropic
'claude-3-5-sonnet-20241022': { inputCostPer1k: 0.003, outputCostPer1k: 0.015 },
'claude-3-5-haiku-20241022': { inputCostPer1k: 0.001, outputCostPer1k: 0.005 },
'claude-3-opus-20240229': { inputCostPer1k: 0.015, outputCostPer1k: 0.075 },
'claude-3-sonnet-20240229': { inputCostPer1k: 0.003, outputCostPer1k: 0.015 },
'claude-3-haiku-20240307': { inputCostPer1k: 0.00025, outputCostPer1k: 0.00125 },
// Google
'gemini-2.0-flash': { inputCostPer1k: 0.0001, outputCostPer1k: 0.0004 },
'gemini-1.5-pro': { inputCostPer1k: 0.00125, outputCostPer1k: 0.005 },
'gemini-1.5-flash': { inputCostPer1k: 0.000075, outputCostPer1k: 0.0003 },
// Default fallback
'default': { inputCostPer1k: 0.001, outputCostPer1k: 0.002 }
};
/**
* Cost Tracker class
*/
export class CostTracker {
private stats: SessionStats;
private customPricing: Map<string, ModelPricing> = new Map();
constructor() {
this.stats = this.createEmptyStats();
}
private createEmptyStats(): SessionStats {
return {
totalInputTokens: 0,
totalOutputTokens: 0,
totalTokens: 0,
totalCost: 0,
requestCount: 0,
avgLatencyMs: 0,
startTime: Date.now(),
requests: []
};
}
/**
* Add a request's token usage
*/
addUsage(
model: string,
inputTokens: number,
outputTokens: number,
latencyMs?: number
): RequestStats {
const totalTokens = inputTokens + outputTokens;
const cost = this.calculateCost(model, inputTokens, outputTokens);
const request: RequestStats = {
timestamp: Date.now(),
model,
usage: { inputTokens, outputTokens, totalTokens },
cost,
latencyMs
};
this.stats.requests.push(request);
this.stats.totalInputTokens += inputTokens;
this.stats.totalOutputTokens += outputTokens;
this.stats.totalTokens += totalTokens;
this.stats.totalCost += cost;
this.stats.requestCount++;
if (latencyMs !== undefined) {
const totalLatency = this.stats.avgLatencyMs * (this.stats.requestCount - 1) + latencyMs;
this.stats.avgLatencyMs = totalLatency / this.stats.requestCount;
}
return request;
}
/**
* Calculate cost for token usage
*/
calculateCost(model: string, inputTokens: number, outputTokens: number): number {
const pricing = this.getPricing(model);
const inputCost = (inputTokens / 1000) * pricing.inputCostPer1k;
const outputCost = (outputTokens / 1000) * pricing.outputCostPer1k;
return inputCost + outputCost;
}
/**
* Get pricing for a model
*/
getPricing(model: string): ModelPricing {
// Check custom pricing first
if (this.customPricing.has(model)) {
return this.customPricing.get(model)!;
}
// Check built-in pricing
if (MODEL_PRICING[model]) {
return MODEL_PRICING[model];
}
// Try to match partial model names
for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
if (model.includes(key) || key.includes(model)) {
return pricing;
}
}
return MODEL_PRICING['default'];
}
/**
* Set custom pricing for a model
*/
setCustomPricing(model: string, pricing: ModelPricing): void {
this.customPricing.set(model, pricing);
}
/**
* Get current session stats
*/
getStats(): SessionStats {
return { ...this.stats };
}
/**
* Get summary string
*/
getSummary(): string {
const duration = (Date.now() - this.stats.startTime) / 1000;
const lines = [
'=== Cost Summary ===',
`Requests: ${this.stats.requestCount}`,
`Duration: ${duration.toFixed(1)}s`,
'',
'Tokens:',
` Input: ${this.stats.totalInputTokens.toLocaleString()}`,
` Output: ${this.stats.totalOutputTokens.toLocaleString()}`,
` Total: ${this.stats.totalTokens.toLocaleString()}`,
'',
`Total Cost: $${this.stats.totalCost.toFixed(6)}`,
];
if (this.stats.avgLatencyMs > 0) {
lines.push(`Avg Latency: ${this.stats.avgLatencyMs.toFixed(0)}ms`);
}
return lines.join('\n');
}
/**
* Get detailed breakdown by model
*/
getBreakdownByModel(): Record<string, { tokens: number; cost: number; requests: number }> {
const breakdown: Record<string, { tokens: number; cost: number; requests: number }> = {};
for (const request of this.stats.requests) {
if (!breakdown[request.model]) {
breakdown[request.model] = { tokens: 0, cost: 0, requests: 0 };
}
breakdown[request.model].tokens += request.usage.totalTokens;
breakdown[request.model].cost += request.cost;
breakdown[request.model].requests++;
}
return breakdown;
}
/**
* Reset stats
*/
reset(): void {
this.stats = this.createEmptyStats();
}
/**
* Export stats to JSON
*/
toJSON(): SessionStats {
return this.getStats();
}
/**
* Import stats from JSON
*/
fromJSON(data: SessionStats): void {
this.stats = { ...data };
}
// Legacy interface compatibility
get totalTokens(): number {
return this.stats.totalTokens;
}
get totalCost(): number {
return this.stats.totalCost;
}
get requests(): number {
return this.stats.requestCount;
}
}
/**
* Create a cost tracker instance
*/
export function createCostTracker(): CostTracker {
return new CostTracker();
}
/**
* Estimate tokens from text (rough approximation)
*/
export function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
/**
* Format cost as currency string
*/
export function formatCost(cost: number): string {
if (cost < 0.01) {
return `$${cost.toFixed(6)}`;
}
if (cost < 1) {
return `$${cost.toFixed(4)}`;
}
return `$${cost.toFixed(2)}`;
}
@@ -0,0 +1,323 @@
/**
* External Agents - Integration adapters for external AI CLI tools
*/
export interface ExternalAgentConfig {
name: string;
command: string;
args?: string[];
cwd?: string;
env?: Record<string, string>;
timeout?: number;
}
export interface ExternalAgentResult {
success: boolean;
output: string;
error?: string;
exitCode: number;
duration: number;
}
/**
* Base class for external agent integrations
*/
export abstract class BaseExternalAgent {
protected config: ExternalAgentConfig;
constructor(config: ExternalAgentConfig) {
this.config = {
timeout: 300000, // 5 minutes default
...config
};
}
/**
* Check if the external tool is available
*/
abstract isAvailable(): Promise<boolean>;
/**
* Execute a prompt with the external agent
*/
abstract execute(prompt: string): Promise<ExternalAgentResult>;
/**
* Get the agent name
*/
getName(): string {
return this.config.name;
}
/**
* Execute a command and return result
*/
protected async runCommand(args: string[]): Promise<ExternalAgentResult> {
const { spawn } = await import('child_process');
const startTime = Date.now();
return new Promise((resolve) => {
const proc = spawn(this.config.command, args, {
cwd: this.config.cwd || process.cwd(),
env: { ...process.env, ...this.config.env },
timeout: this.config.timeout,
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
proc.stdout?.on('data', (data) => {
stdout += data.toString();
});
proc.stderr?.on('data', (data) => {
stderr += data.toString();
});
proc.on('close', (code) => {
resolve({
success: code === 0,
output: stdout,
error: stderr || undefined,
exitCode: code || 0,
duration: Date.now() - startTime
});
});
proc.on('error', (error) => {
resolve({
success: false,
output: '',
error: error.message,
exitCode: 1,
duration: Date.now() - startTime
});
});
});
}
/**
* Check if a command exists
*/
protected async commandExists(command: string): Promise<boolean> {
const { spawn } = await import('child_process');
return new Promise((resolve) => {
const proc = spawn('which', [command], { stdio: 'pipe' });
proc.on('close', (code) => resolve(code === 0));
proc.on('error', () => resolve(false));
});
}
}
/**
* Claude Code CLI integration
*/
export class ClaudeCodeAgent extends BaseExternalAgent {
constructor(cwd?: string) {
super({
name: 'claude-code',
command: 'claude',
cwd
});
}
async isAvailable(): Promise<boolean> {
return this.commandExists('claude');
}
async execute(prompt: string): Promise<ExternalAgentResult> {
return this.runCommand(['--print', prompt]);
}
async executeWithSession(prompt: string, sessionId?: string): Promise<ExternalAgentResult> {
const args = ['--print'];
if (sessionId) {
args.push('--continue', sessionId);
}
args.push(prompt);
return this.runCommand(args);
}
}
/**
* Gemini CLI integration
*/
export class GeminiCliAgent extends BaseExternalAgent {
private model: string;
constructor(cwd?: string, model: string = 'gemini-2.5-pro') {
super({
name: 'gemini-cli',
command: 'gemini',
cwd
});
this.model = model;
}
async isAvailable(): Promise<boolean> {
return this.commandExists('gemini');
}
async execute(prompt: string): Promise<ExternalAgentResult> {
return this.runCommand(['-m', this.model, prompt]);
}
}
/**
* OpenAI Codex CLI integration
*/
export class CodexCliAgent extends BaseExternalAgent {
constructor(cwd?: string) {
super({
name: 'codex-cli',
command: 'codex',
cwd
});
}
async isAvailable(): Promise<boolean> {
return this.commandExists('codex');
}
async execute(prompt: string): Promise<ExternalAgentResult> {
return this.runCommand(['exec', '--full-auto', prompt]);
}
}
/**
* Aider CLI integration
*/
export class AiderAgent extends BaseExternalAgent {
constructor(cwd?: string) {
super({
name: 'aider',
command: 'aider',
cwd
});
}
async isAvailable(): Promise<boolean> {
return this.commandExists('aider');
}
async execute(prompt: string): Promise<ExternalAgentResult> {
return this.runCommand(['--message', prompt, '--yes']);
}
}
/**
* Generic external agent for any CLI tool
*/
export class GenericExternalAgent extends BaseExternalAgent {
private promptArg: string;
constructor(config: ExternalAgentConfig & { promptArg?: string }) {
super(config);
this.promptArg = config.promptArg || '';
}
async isAvailable(): Promise<boolean> {
return this.commandExists(this.config.command);
}
async execute(prompt: string): Promise<ExternalAgentResult> {
const args = [...(this.config.args || [])];
if (this.promptArg) {
args.push(this.promptArg, prompt);
} else {
args.push(prompt);
}
return this.runCommand(args);
}
}
/**
* External Agent Registry
*/
class ExternalAgentRegistry {
private agents: Map<string, () => BaseExternalAgent> = new Map();
constructor() {
// Register built-in agents
this.register('claude', () => new ClaudeCodeAgent());
this.register('gemini', () => new GeminiCliAgent());
this.register('codex', () => new CodexCliAgent());
this.register('aider', () => new AiderAgent());
}
register(name: string, factory: () => BaseExternalAgent): void {
this.agents.set(name, factory);
}
get(name: string): BaseExternalAgent | undefined {
const factory = this.agents.get(name);
return factory ? factory() : undefined;
}
list(): string[] {
return Array.from(this.agents.keys());
}
async getAvailable(): Promise<string[]> {
const available: string[] = [];
for (const [name, factory] of this.agents) {
const agent = factory();
if (await agent.isAvailable()) {
available.push(name);
}
}
return available;
}
}
let registry: ExternalAgentRegistry | null = null;
export function getExternalAgentRegistry(): ExternalAgentRegistry {
if (!registry) {
registry = new ExternalAgentRegistry();
}
return registry;
}
/**
* Create an external agent by name
*/
export function createExternalAgent(name: string, cwd?: string): BaseExternalAgent | undefined {
const reg = getExternalAgentRegistry();
switch (name) {
case 'claude':
return new ClaudeCodeAgent(cwd);
case 'gemini':
return new GeminiCliAgent(cwd);
case 'codex':
return new CodexCliAgent(cwd);
case 'aider':
return new AiderAgent(cwd);
default:
return reg.get(name);
}
}
/**
* Convert external agent to tool for use with PraisonAI agents
*/
export function externalAgentAsTool(agent: BaseExternalAgent): {
name: string;
description: string;
execute: (input: string) => Promise<string>;
} {
return {
name: `external_${agent.getName()}`,
description: `Execute a prompt using the ${agent.getName()} external agent`,
execute: async (input: string) => {
const result = await agent.execute(input);
if (!result.success) {
throw new Error(result.error || 'External agent execution failed');
}
return result.output;
}
};
}
@@ -0,0 +1,391 @@
/**
* Fast Context - Fast retrieval and summarization pipeline
* Minimizes tokens and latency with deterministic caching
*/
export interface FastContextConfig {
maxTokens?: number;
cacheEnabled?: boolean;
cacheTTL?: number;
summarizer?: (text: string, maxTokens: number) => Promise<string>;
embeddingProvider?: {
embed: (text: string) => Promise<number[]>;
};
}
export interface ContextSource {
id: string;
type: 'memory' | 'knowledge' | 'file' | 'custom';
content: string;
relevance?: number;
metadata?: Record<string, any>;
}
export interface FastContextResult {
context: string;
sources: ContextSource[];
tokenCount: number;
cached: boolean;
latencyMs: number;
}
export interface CacheEntry {
result: FastContextResult;
timestamp: number;
hits: number;
}
/**
* Fast Context class for efficient context retrieval
*/
export class FastContext {
private config: FastContextConfig;
private cache: Map<string, CacheEntry> = new Map();
private sources: Map<string, ContextSource[]> = new Map();
constructor(config: FastContextConfig = {}) {
this.config = {
maxTokens: 4000,
cacheEnabled: true,
cacheTTL: 300000, // 5 minutes
...config
};
}
/**
* Register a context source
*/
registerSource(
id: string,
type: ContextSource['type'],
contents: string[],
metadata?: Record<string, any>
): void {
const sources: ContextSource[] = contents.map((content, i) => ({
id: `${id}_${i}`,
type,
content,
metadata
}));
this.sources.set(id, sources);
}
/**
* Register memory as a source
*/
registerMemory(memory: { getAll: () => Array<{ content: string; metadata?: any }> }): void {
const entries = memory.getAll();
const sources: ContextSource[] = entries.map((entry, i) => ({
id: `memory_${i}`,
type: 'memory',
content: entry.content,
metadata: entry.metadata
}));
this.sources.set('memory', sources);
}
/**
* Register knowledge base as a source
*/
registerKnowledge(
knowledge: { search: (query: string, limit?: number) => Promise<Array<{ content: string; score: number }>> }
): void {
// Store reference for dynamic retrieval
(this as any)._knowledgeBase = knowledge;
}
/**
* Get context for a query
*/
async getContext(query: string): Promise<FastContextResult> {
const startTime = Date.now();
// Check cache
const cacheKey = this.getCacheKey(query);
if (this.config.cacheEnabled) {
const cached = this.getFromCache(cacheKey);
if (cached) {
return {
...cached.result,
cached: true,
latencyMs: Date.now() - startTime
};
}
}
// Gather relevant sources
const relevantSources = await this.gatherRelevantSources(query);
// Build context within token limit
const { context, sources, tokenCount } = await this.buildContext(relevantSources, query);
const result: FastContextResult = {
context,
sources,
tokenCount,
cached: false,
latencyMs: Date.now() - startTime
};
// Cache result
if (this.config.cacheEnabled) {
this.addToCache(cacheKey, result);
}
return result;
}
/**
* Gather relevant sources for a query
*/
private async gatherRelevantSources(query: string): Promise<ContextSource[]> {
const allSources: ContextSource[] = [];
// Get from registered sources
for (const sources of this.sources.values()) {
allSources.push(...sources);
}
// Get from knowledge base if registered
const kb = (this as any)._knowledgeBase;
if (kb) {
try {
const kbResults = await kb.search(query, 10);
for (let i = 0; i < kbResults.length; i++) {
allSources.push({
id: `kb_${i}`,
type: 'knowledge',
content: kbResults[i].content,
relevance: kbResults[i].score
});
}
} catch {
// Ignore knowledge base errors
}
}
// Score relevance if embedding provider available
if (this.config.embeddingProvider) {
await this.scoreRelevance(allSources, query);
} else {
// Simple text matching fallback
this.scoreRelevanceSimple(allSources, query);
}
// Sort by relevance
allSources.sort((a, b) => (b.relevance || 0) - (a.relevance || 0));
return allSources;
}
/**
* Score relevance using embeddings
*/
private async scoreRelevance(sources: ContextSource[], query: string): Promise<void> {
if (!this.config.embeddingProvider) return;
const queryEmbedding = await this.config.embeddingProvider.embed(query);
for (const source of sources) {
if (source.relevance !== undefined) continue; // Already scored
const sourceEmbedding = await this.config.embeddingProvider.embed(source.content);
source.relevance = this.cosineSimilarity(queryEmbedding, sourceEmbedding);
}
}
/**
* Simple text-based relevance scoring
*/
private scoreRelevanceSimple(sources: ContextSource[], query: string): void {
const queryWords = query.toLowerCase().split(/\s+/);
for (const source of sources) {
if (source.relevance !== undefined) continue;
const contentLower = source.content.toLowerCase();
let score = 0;
for (const word of queryWords) {
if (contentLower.includes(word)) {
score += 1 / queryWords.length;
}
}
// Boost for exact phrase match
if (contentLower.includes(query.toLowerCase())) {
score += 0.5;
}
source.relevance = Math.min(score, 1);
}
}
/**
* Build context from sources within token limit
*/
private async buildContext(
sources: ContextSource[],
query: string
): Promise<{ context: string; sources: ContextSource[]; tokenCount: number }> {
const maxTokens = this.config.maxTokens || 4000;
const usedSources: ContextSource[] = [];
const contextParts: string[] = [];
let tokenCount = 0;
for (const source of sources) {
const sourceTokens = this.estimateTokens(source.content);
if (tokenCount + sourceTokens <= maxTokens) {
contextParts.push(source.content);
usedSources.push(source);
tokenCount += sourceTokens;
} else if (this.config.summarizer && sourceTokens > 100) {
// Try to summarize if too long
const remainingTokens = maxTokens - tokenCount;
if (remainingTokens > 50) {
const summary = await this.config.summarizer(source.content, remainingTokens);
const summaryTokens = this.estimateTokens(summary);
if (tokenCount + summaryTokens <= maxTokens) {
contextParts.push(summary);
usedSources.push({ ...source, content: summary, metadata: { ...source.metadata, summarized: true } });
tokenCount += summaryTokens;
}
}
}
if (tokenCount >= maxTokens * 0.95) break;
}
return {
context: contextParts.join('\n\n'),
sources: usedSources,
tokenCount
};
}
/**
* Get cache key for query
*/
private getCacheKey(query: string): string {
// Simple hash
let hash = 0;
for (let i = 0; i < query.length; i++) {
const char = query.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return `ctx_${hash}`;
}
/**
* Get from cache
*/
private getFromCache(key: string): CacheEntry | null {
const entry = this.cache.get(key);
if (!entry) return null;
// Check TTL
if (Date.now() - entry.timestamp > (this.config.cacheTTL || 300000)) {
this.cache.delete(key);
return null;
}
entry.hits++;
return entry;
}
/**
* Add to cache
*/
private addToCache(key: string, result: FastContextResult): void {
this.cache.set(key, {
result,
timestamp: Date.now(),
hits: 0
});
// Limit cache size
if (this.cache.size > 100) {
const oldest = Array.from(this.cache.entries())
.sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
if (oldest) {
this.cache.delete(oldest[0]);
}
}
}
/**
* Clear cache
*/
clearCache(): void {
this.cache.clear();
}
/**
* Get cache stats
*/
getCacheStats(): { size: number; totalHits: number } {
let totalHits = 0;
for (const entry of this.cache.values()) {
totalHits += entry.hits;
}
return { size: this.cache.size, totalHits };
}
/**
* Estimate token count
*/
private estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
/**
* Cosine similarity
*/
private cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) return 0;
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
return denominator === 0 ? 0 : dotProduct / denominator;
}
/**
* Clear all sources
*/
clearSources(): void {
this.sources.clear();
(this as any)._knowledgeBase = undefined;
}
}
/**
* Create a fast context instance
*/
export function createFastContext(config?: FastContextConfig): FastContext {
return new FastContext(config);
}
/**
* Quick context retrieval
*/
export async function getQuickContext(
query: string,
sources: string[],
maxTokens?: number
): Promise<string> {
const fc = createFastContext({ maxTokens, cacheEnabled: false });
fc.registerSource('quick', 'custom', sources);
const result = await fc.getContext(query);
return result.context;
}
@@ -0,0 +1,319 @@
/**
* Flow Display - Textual DAG visualization for workflows
*/
export interface FlowNode {
id: string;
name: string;
type: 'agent' | 'tool' | 'condition' | 'parallel' | 'start' | 'end';
status?: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
children?: string[];
metadata?: Record<string, any>;
}
export interface FlowGraph {
nodes: Map<string, FlowNode>;
edges: Array<{ from: string; to: string; label?: string }>;
}
export interface FlowDisplayConfig {
showStatus?: boolean;
showMetadata?: boolean;
compact?: boolean;
maxWidth?: number;
}
/**
* Flow Display class for DAG visualization
*/
export class FlowDisplay {
private graph: FlowGraph;
private config: FlowDisplayConfig;
constructor(config: FlowDisplayConfig = {}) {
this.config = {
showStatus: true,
showMetadata: false,
compact: false,
maxWidth: 80,
...config
};
this.graph = {
nodes: new Map(),
edges: []
};
}
/**
* Add a node to the graph
*/
addNode(node: FlowNode): void {
this.graph.nodes.set(node.id, node);
}
/**
* Add an edge between nodes
*/
addEdge(from: string, to: string, label?: string): void {
this.graph.edges.push({ from, to, label });
}
/**
* Update node status
*/
updateStatus(id: string, status: FlowNode['status']): void {
const node = this.graph.nodes.get(id);
if (node) {
node.status = status;
}
}
/**
* Build graph from workflow steps
*/
fromWorkflowSteps(steps: Array<{ name: string; type?: string; condition?: string }>): void {
this.graph.nodes.clear();
this.graph.edges = [];
// Add start node
this.addNode({ id: 'start', name: 'Start', type: 'start' });
let prevId = 'start';
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
const id = `step_${i}`;
this.addNode({
id,
name: step.name,
type: (step.type as FlowNode['type']) || 'agent',
status: 'pending'
});
this.addEdge(prevId, id, step.condition);
prevId = id;
}
// Add end node
this.addNode({ id: 'end', name: 'End', type: 'end' });
this.addEdge(prevId, 'end');
}
/**
* Render the graph as text
*/
render(): string {
const lines: string[] = [];
const visited = new Set<string>();
// Find root nodes (nodes with no incoming edges)
const hasIncoming = new Set(this.graph.edges.map(e => e.to));
const roots = Array.from(this.graph.nodes.keys()).filter(id => !hasIncoming.has(id));
if (roots.length === 0 && this.graph.nodes.size > 0) {
const firstKey = this.graph.nodes.keys().next().value;
if (firstKey) roots.push(firstKey);
}
for (const root of roots) {
this.renderNode(root, '', true, lines, visited);
}
return lines.join('\n');
}
/**
* Render a single node and its children
*/
private renderNode(
id: string,
prefix: string,
isLast: boolean,
lines: string[],
visited: Set<string>
): void {
if (visited.has(id)) {
lines.push(`${prefix}${isLast ? '└── ' : '├── '}(cycle: ${id})`);
return;
}
visited.add(id);
const node = this.graph.nodes.get(id);
if (!node) return;
const connector = isLast ? '└── ' : '├── ';
const nodeStr = this.formatNode(node);
lines.push(`${prefix}${connector}${nodeStr}`);
// Find children
const childEdges = this.graph.edges.filter(e => e.from === id);
const childPrefix = prefix + (isLast ? ' ' : '│ ');
for (let i = 0; i < childEdges.length; i++) {
const edge = childEdges[i];
const isLastChild = i === childEdges.length - 1;
if (edge.label) {
lines.push(`${childPrefix}${isLastChild ? '└' : '├'}─[${edge.label}]─┐`);
this.renderNode(edge.to, childPrefix + (isLastChild ? ' ' : '│ '), true, lines, visited);
} else {
this.renderNode(edge.to, childPrefix, isLastChild, lines, visited);
}
}
}
/**
* Format a node for display
*/
private formatNode(node: FlowNode): string {
const parts: string[] = [];
// Status indicator
if (this.config.showStatus && node.status) {
const statusIcons: Record<string, string> = {
pending: '○',
running: '◐',
completed: '●',
failed: '✗',
skipped: '◌'
};
parts.push(statusIcons[node.status] || '?');
}
// Type indicator
const typeIcons: Record<string, string> = {
agent: '🤖',
tool: '🔧',
condition: '❓',
parallel: '⫘',
start: '▶',
end: '■'
};
if (!this.config.compact) {
parts.push(typeIcons[node.type] || '□');
}
// Name
parts.push(node.name);
// Metadata
if (this.config.showMetadata && node.metadata) {
const meta = Object.entries(node.metadata)
.map(([k, v]) => `${k}=${v}`)
.join(', ');
if (meta) {
parts.push(`(${meta})`);
}
}
return parts.join(' ');
}
/**
* Render as simple ASCII box diagram
*/
renderBoxes(): string {
const lines: string[] = [];
const nodeList = Array.from(this.graph.nodes.values());
for (let i = 0; i < nodeList.length; i++) {
const node = nodeList[i];
const box = this.createBox(node);
lines.push(...box);
if (i < nodeList.length - 1) {
lines.push(' │');
lines.push(' ▼');
}
}
return lines.join('\n');
}
/**
* Create a box for a node
*/
private createBox(node: FlowNode): string[] {
const name = node.name;
const width = Math.max(name.length + 4, 20);
const padding = Math.floor((width - name.length - 2) / 2);
const statusChar = this.config.showStatus ? this.getStatusChar(node.status) : '';
const top = '┌' + '─'.repeat(width) + '┐';
const middle = '│' + ' '.repeat(padding) + statusChar + name + ' '.repeat(width - padding - name.length - statusChar.length) + '│';
const bottom = '└' + '─'.repeat(width) + '┘';
return [top, middle, bottom];
}
/**
* Get status character
*/
private getStatusChar(status?: string): string {
const chars: Record<string, string> = {
pending: '○ ',
running: '◐ ',
completed: '● ',
failed: '✗ ',
skipped: '◌ '
};
return status ? (chars[status] || '') : '';
}
/**
* Clear the graph
*/
clear(): void {
this.graph.nodes.clear();
this.graph.edges = [];
}
/**
* Get graph data
*/
getGraph(): FlowGraph {
return this.graph;
}
/**
* Export to DOT format (for Graphviz)
*/
toDot(): string {
const lines: string[] = ['digraph workflow {'];
lines.push(' rankdir=TB;');
lines.push(' node [shape=box];');
for (const [id, node] of this.graph.nodes) {
const label = node.name.replace(/"/g, '\\"');
const style = node.status === 'completed' ? 'filled' : '';
const fillcolor = node.status === 'completed' ? 'lightgreen' :
node.status === 'failed' ? 'lightcoral' : 'white';
lines.push(` "${id}" [label="${label}" style="${style}" fillcolor="${fillcolor}"];`);
}
for (const edge of this.graph.edges) {
const label = edge.label ? ` [label="${edge.label}"]` : '';
lines.push(` "${edge.from}" -> "${edge.to}"${label};`);
}
lines.push('}');
return lines.join('\n');
}
}
/**
* Create a flow display instance
*/
export function createFlowDisplay(config?: FlowDisplayConfig): FlowDisplay {
return new FlowDisplay(config);
}
/**
* Quick render workflow steps
*/
export function renderWorkflow(steps: Array<{ name: string; type?: string }>): string {
const display = createFlowDisplay();
display.fromWorkflowSteps(steps);
return display.render();
}
@@ -0,0 +1,416 @@
/**
* Git Integration - Git operations for CLI
* Safe, read-focused operations with optional write capabilities
*/
export interface GitConfig {
cwd?: string;
safe?: boolean; // If true, only allow read operations
}
export interface GitStatus {
branch: string;
ahead: number;
behind: number;
staged: string[];
modified: string[];
untracked: string[];
deleted: string[];
conflicts: string[];
}
export interface GitCommit {
hash: string;
shortHash: string;
author: string;
email: string;
date: Date;
message: string;
}
export interface GitDiff {
files: GitDiffFile[];
summary: string;
}
export interface GitDiffFile {
path: string;
status: 'added' | 'modified' | 'deleted' | 'renamed';
additions: number;
deletions: number;
diff?: string;
}
/**
* Git Manager class
*/
export class GitManager {
private cwd: string;
private safe: boolean;
constructor(config: GitConfig = {}) {
this.cwd = config.cwd || process.cwd();
this.safe = config.safe ?? true;
}
/**
* Execute a git command
*/
private async exec(args: string[]): Promise<string> {
const { spawn } = await import('child_process');
return new Promise((resolve, reject) => {
const proc = spawn('git', args, {
cwd: this.cwd,
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
proc.stdout?.on('data', (data) => {
stdout += data.toString();
});
proc.stderr?.on('data', (data) => {
stderr += data.toString();
});
proc.on('close', (code) => {
if (code === 0) {
resolve(stdout.trim());
} else {
reject(new Error(stderr || `Git command failed with code ${code}`));
}
});
proc.on('error', reject);
});
}
/**
* Check if directory is a git repository
*/
async isRepo(): Promise<boolean> {
try {
await this.exec(['rev-parse', '--git-dir']);
return true;
} catch {
return false;
}
}
/**
* Get current branch name
*/
async getBranch(): Promise<string> {
return this.exec(['rev-parse', '--abbrev-ref', 'HEAD']);
}
/**
* Get repository status
*/
async getStatus(): Promise<GitStatus> {
const branch = await this.getBranch();
const porcelain = await this.exec(['status', '--porcelain', '-b']);
const lines = porcelain.split('\n');
const status: GitStatus = {
branch,
ahead: 0,
behind: 0,
staged: [],
modified: [],
untracked: [],
deleted: [],
conflicts: []
};
// Parse branch line for ahead/behind
const branchLine = lines[0];
const aheadMatch = branchLine.match(/ahead (\d+)/);
const behindMatch = branchLine.match(/behind (\d+)/);
if (aheadMatch) status.ahead = parseInt(aheadMatch[1]);
if (behindMatch) status.behind = parseInt(behindMatch[1]);
// Parse file status
for (const line of lines.slice(1)) {
if (!line) continue;
const indexStatus = line[0];
const workStatus = line[1];
const file = line.slice(3);
if (indexStatus === 'U' || workStatus === 'U') {
status.conflicts.push(file);
} else if (indexStatus === '?' && workStatus === '?') {
status.untracked.push(file);
} else if (indexStatus === 'D' || workStatus === 'D') {
status.deleted.push(file);
} else if (indexStatus !== ' ') {
status.staged.push(file);
} else if (workStatus === 'M') {
status.modified.push(file);
}
}
return status;
}
/**
* Get diff (staged or unstaged)
*/
async getDiff(staged: boolean = false): Promise<GitDiff> {
const args = ['diff', '--stat'];
if (staged) args.push('--staged');
const stat = await this.exec(args);
const files: GitDiffFile[] = [];
const lines = stat.split('\n');
for (const line of lines) {
const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)\s*([+-]*)/);
if (match) {
const [, path, changes, indicators] = match;
const additions = (indicators.match(/\+/g) || []).length;
const deletions = (indicators.match(/-/g) || []).length;
files.push({
path: path.trim(),
status: 'modified',
additions,
deletions
});
}
}
// Get full diff for summary
const fullDiffArgs = ['diff'];
if (staged) fullDiffArgs.push('--staged');
const fullDiff = await this.exec(fullDiffArgs);
return {
files,
summary: fullDiff
};
}
/**
* Get recent commits
*/
async getLog(limit: number = 10): Promise<GitCommit[]> {
const format = '%H|%h|%an|%ae|%aI|%s';
const output = await this.exec(['log', `-${limit}`, `--format=${format}`]);
return output.split('\n').filter(Boolean).map(line => {
const [hash, shortHash, author, email, date, message] = line.split('|');
return {
hash,
shortHash,
author,
email,
date: new Date(date),
message
};
});
}
/**
* Get list of branches
*/
async getBranches(): Promise<{ name: string; current: boolean }[]> {
const output = await this.exec(['branch', '--list']);
return output.split('\n').filter(Boolean).map(line => ({
name: line.replace(/^\*?\s*/, '').trim(),
current: line.startsWith('*')
}));
}
/**
* Stage files (requires safe=false)
*/
async add(files: string[] = ['.']): Promise<void> {
if (this.safe) {
throw new Error('Write operations disabled in safe mode');
}
await this.exec(['add', ...files]);
}
/**
* Commit staged changes (requires safe=false)
*/
async commit(message: string): Promise<string> {
if (this.safe) {
throw new Error('Write operations disabled in safe mode');
}
return this.exec(['commit', '-m', message]);
}
/**
* Stash changes (requires safe=false)
*/
async stash(message?: string): Promise<void> {
if (this.safe) {
throw new Error('Write operations disabled in safe mode');
}
const args = ['stash', 'push'];
if (message) args.push('-m', message);
await this.exec(args);
}
/**
* Pop stash (requires safe=false)
*/
async stashPop(): Promise<void> {
if (this.safe) {
throw new Error('Write operations disabled in safe mode');
}
await this.exec(['stash', 'pop']);
}
/**
* Get stash list
*/
async getStashList(): Promise<string[]> {
const output = await this.exec(['stash', 'list']);
return output.split('\n').filter(Boolean);
}
/**
* Get file content at a specific commit
*/
async show(ref: string, path: string): Promise<string> {
return this.exec(['show', `${ref}:${path}`]);
}
/**
* Get blame for a file
*/
async blame(path: string): Promise<string> {
return this.exec(['blame', path]);
}
/**
* Generate commit message from staged changes
*/
async generateCommitMessage(): Promise<string> {
const diff = await this.getDiff(true);
if (diff.files.length === 0) {
return 'No staged changes';
}
const fileList = diff.files.map(f => f.path).join(', ');
const totalAdditions = diff.files.reduce((sum, f) => sum + f.additions, 0);
const totalDeletions = diff.files.reduce((sum, f) => sum + f.deletions, 0);
// Simple template-based message
if (diff.files.length === 1) {
return `Update ${diff.files[0].path}`;
}
return `Update ${diff.files.length} files (+${totalAdditions}/-${totalDeletions})`;
}
/**
* Get formatted status string
*/
async getStatusString(): Promise<string> {
const status = await this.getStatus();
const lines: string[] = [];
lines.push(`Branch: ${status.branch}`);
if (status.ahead > 0) lines.push(` ↑ ${status.ahead} ahead`);
if (status.behind > 0) lines.push(` ↓ ${status.behind} behind`);
if (status.staged.length > 0) {
lines.push(`\nStaged (${status.staged.length}):`);
status.staged.forEach(f => lines.push(` + ${f}`));
}
if (status.modified.length > 0) {
lines.push(`\nModified (${status.modified.length}):`);
status.modified.forEach(f => lines.push(` M ${f}`));
}
if (status.untracked.length > 0) {
lines.push(`\nUntracked (${status.untracked.length}):`);
status.untracked.forEach(f => lines.push(` ? ${f}`));
}
if (status.deleted.length > 0) {
lines.push(`\nDeleted (${status.deleted.length}):`);
status.deleted.forEach(f => lines.push(` - ${f}`));
}
if (status.conflicts.length > 0) {
lines.push(`\nConflicts (${status.conflicts.length}):`);
status.conflicts.forEach(f => lines.push(` ! ${f}`));
}
return lines.join('\n');
}
}
/**
* Create a git manager instance
*/
export function createGitManager(config?: GitConfig): GitManager {
return new GitManager(config);
}
/**
* Diff viewer with syntax highlighting (text-based)
*/
export class DiffViewer {
private diff: string;
constructor(diff: string) {
this.diff = diff;
}
/**
* Get formatted diff with markers
*/
format(): string {
const lines = this.diff.split('\n');
return lines.map(line => {
if (line.startsWith('+') && !line.startsWith('+++')) {
return `[+] ${line}`;
}
if (line.startsWith('-') && !line.startsWith('---')) {
return `[-] ${line}`;
}
if (line.startsWith('@@')) {
return `[~] ${line}`;
}
return ` ${line}`;
}).join('\n');
}
/**
* Get summary statistics
*/
getStats(): { additions: number; deletions: number } {
let additions = 0;
let deletions = 0;
for (const line of this.diff.split('\n')) {
if (line.startsWith('+') && !line.startsWith('+++')) {
additions++;
}
if (line.startsWith('-') && !line.startsWith('---')) {
deletions++;
}
}
return { additions, deletions };
}
}
/**
* Create a diff viewer
*/
export function createDiffViewer(diff: string): DiffViewer {
return new DiffViewer(diff);
}
+179
View File
@@ -0,0 +1,179 @@
/**
* CLI Features - Lazy-loaded feature modules
*/
// Slash Commands
export {
SlashCommandHandler,
createSlashCommandHandler,
registerCommand,
parseSlashCommand,
executeSlashCommand,
isSlashCommand,
getRegistry,
type SlashCommand,
type SlashCommandContext,
type SlashCommandResult,
type CostTracker as SlashCostTracker
} from './slash-commands';
// Cost Tracker
export {
CostTracker,
createCostTracker,
estimateTokens,
formatCost,
MODEL_PRICING,
type ModelPricing,
type TokenUsage,
type RequestStats,
type SessionStats
} from './cost-tracker';
// Interactive TUI
export {
InteractiveTUI,
createInteractiveTUI,
StatusDisplay,
createStatusDisplay,
HistoryManager,
createHistoryManager,
type TUIConfig,
type TUIState
} from './interactive-tui';
// Repo Map
export {
RepoMap,
createRepoMap,
getRepoTree,
DEFAULT_IGNORE_PATTERNS,
type RepoMapConfig,
type FileInfo,
type SymbolInfo,
type RepoMapResult
} from './repo-map';
// Git Integration
export {
GitManager,
createGitManager,
DiffViewer,
createDiffViewer,
type GitConfig,
type GitStatus,
type GitCommit,
type GitDiff,
type GitDiffFile
} from './git-integration';
// Sandbox Executor
export {
SandboxExecutor,
createSandboxExecutor,
sandboxExec,
CommandValidator,
DEFAULT_BLOCKED_COMMANDS,
DEFAULT_BLOCKED_PATHS,
type SandboxMode,
type SandboxConfig,
type ExecutionResult
} from './sandbox-executor';
// Autonomy Mode
export {
AutonomyManager,
createAutonomyManager,
cliApprovalPrompt,
MODE_POLICIES,
type AutonomyMode,
type ActionType,
type ApprovalPolicy,
type AutonomyConfig,
type ActionRequest,
type ActionDecision
} from './autonomy-mode';
// Scheduler
export {
Scheduler,
createScheduler,
cronExpressions,
type ScheduleConfig,
type ScheduledTask,
type SchedulerStats
} from './scheduler';
// Background Jobs
export {
JobQueue,
createJobQueue,
MemoryJobStorage,
FileJobStorage,
createFileJobStorage,
type Job,
type JobStatus,
type JobPriority,
type JobQueueConfig,
type JobStorageAdapter,
type JobHandler,
type JobContext
} from './background-jobs';
// Checkpoints
export {
CheckpointManager,
createCheckpointManager,
MemoryCheckpointStorage,
FileCheckpointStorage,
createFileCheckpointStorage,
type CheckpointData,
type CheckpointConfig,
type CheckpointStorage
} from './checkpoints';
// Flow Display
export {
FlowDisplay,
createFlowDisplay,
renderWorkflow,
type FlowNode,
type FlowGraph,
type FlowDisplayConfig
} from './flow-display';
// External Agents
export {
BaseExternalAgent,
ClaudeCodeAgent,
GeminiCliAgent,
CodexCliAgent,
AiderAgent,
GenericExternalAgent,
getExternalAgentRegistry,
createExternalAgent,
externalAgentAsTool,
type ExternalAgentConfig,
type ExternalAgentResult
} from './external-agents';
// N8N Integration
export {
N8NIntegration,
createN8NIntegration,
triggerN8NWebhook,
type N8NConfig,
type N8NWebhookPayload,
type N8NWorkflow,
type N8NWorkflowNode
} from './n8n-integration';
// Fast Context
export {
FastContext,
createFastContext,
getQuickContext,
type FastContextConfig,
type ContextSource,
type FastContextResult
} from './fast-context';
@@ -0,0 +1,343 @@
/**
* Interactive TUI - Terminal User Interface for CLI
* Minimal but functional interactive mode with readline
*/
import { SlashCommandHandler, isSlashCommand, SlashCommandContext } from './slash-commands';
import { CostTracker, createCostTracker } from './cost-tracker';
export interface TUIConfig {
prompt?: string;
welcomeMessage?: string;
historyFile?: string;
maxHistorySize?: number;
onMessage?: (message: string) => Promise<string>;
onExit?: () => void;
}
export interface TUIState {
running: boolean;
history: string[];
context: SlashCommandContext;
}
/**
* Interactive TUI class
*/
export class InteractiveTUI {
private config: TUIConfig;
private state: TUIState;
private slashHandler: SlashCommandHandler;
private costTracker: CostTracker;
private readline: any;
private rl: any;
constructor(config: TUIConfig = {}) {
this.config = {
prompt: '> ',
welcomeMessage: 'PraisonAI Interactive Mode. Type /help for commands, /exit to quit.',
maxHistorySize: 1000,
...config
};
this.costTracker = createCostTracker();
this.state = {
running: false,
history: [],
context: {
history: [],
settings: {},
costTracker: this.costTracker,
onOutput: (msg) => this.output(msg)
}
};
this.slashHandler = new SlashCommandHandler(this.state.context);
}
/**
* Start the interactive session
*/
async start(): Promise<void> {
if (this.state.running) return;
// Lazy load readline
this.readline = await import('readline');
this.rl = this.readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: true
});
this.state.running = true;
// Load history if available
await this.loadHistory();
// Show welcome message
if (this.config.welcomeMessage) {
this.output(this.config.welcomeMessage);
}
// Start the input loop
await this.inputLoop();
}
/**
* Stop the interactive session
*/
stop(): void {
this.state.running = false;
if (this.rl) {
this.rl.close();
}
this.saveHistory();
this.config.onExit?.();
}
/**
* Main input loop
*/
private async inputLoop(): Promise<void> {
while (this.state.running) {
const input = await this.prompt();
if (input === null) {
// EOF or Ctrl+C
this.stop();
break;
}
const trimmed = input.trim();
if (!trimmed) continue;
// Add to history
this.addToHistory(trimmed);
// Handle slash commands
if (isSlashCommand(trimmed)) {
const result = await this.slashHandler.handle(trimmed);
if (result.shouldExit) {
this.stop();
break;
}
continue;
}
// Handle regular message
if (this.config.onMessage) {
try {
const response = await this.config.onMessage(trimmed);
this.output(response);
} catch (error: any) {
this.output(`Error: ${error.message}`);
}
} else {
this.output('No message handler configured. Use /help for available commands.');
}
}
}
/**
* Prompt for input
*/
private prompt(): Promise<string | null> {
return new Promise((resolve) => {
if (!this.state.running || !this.rl) {
resolve(null);
return;
}
this.rl.question(this.config.prompt, (answer: string) => {
resolve(answer);
});
// Handle Ctrl+C
this.rl.once('close', () => {
resolve(null);
});
});
}
/**
* Output a message
*/
private output(message: string): void {
console.log(message);
}
/**
* Add input to history
*/
private addToHistory(input: string): void {
this.state.history.push(input);
if (this.state.history.length > (this.config.maxHistorySize || 1000)) {
this.state.history.shift();
}
}
/**
* Load history from file
*/
private async loadHistory(): Promise<void> {
if (!this.config.historyFile) return;
try {
const fs = await import('fs/promises');
const content = await fs.readFile(this.config.historyFile, 'utf-8');
this.state.history = content.trim().split('\n').filter(Boolean);
} catch {
// Ignore if file doesn't exist
}
}
/**
* Save history to file
*/
private async saveHistory(): Promise<void> {
if (!this.config.historyFile) return;
try {
const fs = await import('fs/promises');
await fs.writeFile(this.config.historyFile, this.state.history.join('\n'));
} catch {
// Ignore save errors
}
}
/**
* Get current state
*/
getState(): TUIState {
return { ...this.state };
}
/**
* Get cost tracker
*/
getCostTracker(): CostTracker {
return this.costTracker;
}
/**
* Update context
*/
updateContext(updates: Partial<SlashCommandContext>): void {
this.slashHandler.updateContext(updates);
}
}
/**
* Create an interactive TUI instance
*/
export function createInteractiveTUI(config?: TUIConfig): InteractiveTUI {
return new InteractiveTUI(config);
}
/**
* Simple status display helper
*/
export class StatusDisplay {
private lines: string[] = [];
add(label: string, value: string | number): this {
this.lines.push(`${label}: ${value}`);
return this;
}
addSeparator(): this {
this.lines.push('─'.repeat(40));
return this;
}
addHeader(text: string): this {
this.lines.push(`\n=== ${text} ===`);
return this;
}
toString(): string {
return this.lines.join('\n');
}
print(): void {
console.log(this.toString());
}
clear(): void {
this.lines = [];
}
}
/**
* Create a status display
*/
export function createStatusDisplay(): StatusDisplay {
return new StatusDisplay();
}
/**
* History manager for persistent command history
*/
export class HistoryManager {
private history: string[] = [];
private maxSize: number;
private filePath?: string;
constructor(maxSize: number = 1000, filePath?: string) {
this.maxSize = maxSize;
this.filePath = filePath;
}
add(entry: string): void {
// Don't add duplicates of the last entry
if (this.history[this.history.length - 1] === entry) return;
this.history.push(entry);
if (this.history.length > this.maxSize) {
this.history.shift();
}
}
getAll(): string[] {
return [...this.history];
}
search(query: string): string[] {
const lower = query.toLowerCase();
return this.history.filter(h => h.toLowerCase().includes(lower));
}
async load(): Promise<void> {
if (!this.filePath) return;
try {
const fs = await import('fs/promises');
const content = await fs.readFile(this.filePath, 'utf-8');
this.history = content.trim().split('\n').filter(Boolean);
} catch {
// File doesn't exist yet
}
}
async save(): Promise<void> {
if (!this.filePath) return;
try {
const fs = await import('fs/promises');
await fs.writeFile(this.filePath, this.history.join('\n'));
} catch {
// Ignore save errors
}
}
clear(): void {
this.history = [];
}
}
/**
* Create a history manager
*/
export function createHistoryManager(maxSize?: number, filePath?: string): HistoryManager {
return new HistoryManager(maxSize, filePath);
}
@@ -0,0 +1,351 @@
/**
* N8N Integration - Webhook triggers and workflow export for n8n
*/
export interface N8NConfig {
baseUrl?: string;
apiKey?: string;
webhookPath?: string;
}
export interface N8NWebhookPayload {
event: string;
data: Record<string, any>;
timestamp: string;
source: string;
}
export interface N8NWorkflowNode {
id: string;
name: string;
type: string;
position: [number, number];
parameters: Record<string, any>;
credentials?: Record<string, any>;
}
export interface N8NWorkflowConnection {
node: string;
type: string;
index: number;
}
export interface N8NWorkflow {
name: string;
nodes: N8NWorkflowNode[];
connections: Record<string, { main: N8NWorkflowConnection[][] }>;
settings?: Record<string, any>;
}
/**
* N8N Integration class
*/
export class N8NIntegration {
private config: N8NConfig;
constructor(config: N8NConfig = {}) {
this.config = {
baseUrl: config.baseUrl || process.env.N8N_BASE_URL || 'http://localhost:5678',
apiKey: config.apiKey || process.env.N8N_API_KEY,
webhookPath: config.webhookPath || '/webhook',
...config
};
}
/**
* Trigger a webhook
*/
async triggerWebhook(
webhookId: string,
payload: Record<string, any>
): Promise<{ success: boolean; response?: any; error?: string }> {
const url = `${this.config.baseUrl}${this.config.webhookPath}/${webhookId}`;
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.config.apiKey ? { 'X-N8N-API-KEY': this.config.apiKey } : {})
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const error = await response.text();
return { success: false, error: `HTTP ${response.status}: ${error}` };
}
const data = await response.json().catch(() => ({}));
return { success: true, response: data };
} catch (error: any) {
return { success: false, error: error.message };
}
}
/**
* Create a webhook payload for agent events
*/
createAgentEventPayload(
event: string,
agentId: string,
data: Record<string, any>
): N8NWebhookPayload {
return {
event,
data: {
agentId,
...data
},
timestamp: new Date().toISOString(),
source: 'praisonai'
};
}
/**
* Export PraisonAI workflow to n8n format
*/
exportWorkflow(
name: string,
steps: Array<{
name: string;
type: 'agent' | 'tool' | 'condition';
config?: Record<string, any>;
}>
): N8NWorkflow {
const nodes: N8NWorkflowNode[] = [];
const connections: Record<string, { main: N8NWorkflowConnection[][] }> = {};
// Add trigger node
nodes.push({
id: 'trigger',
name: 'Webhook Trigger',
type: 'n8n-nodes-base.webhook',
position: [250, 300],
parameters: {
httpMethod: 'POST',
path: name.toLowerCase().replace(/\s+/g, '-'),
responseMode: 'onReceived'
}
});
let prevNodeId = 'trigger';
let yPosition = 300;
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
const nodeId = `node_${i}`;
yPosition += 150;
// Map step type to n8n node type
const nodeType = this.mapStepTypeToN8N(step.type);
nodes.push({
id: nodeId,
name: step.name,
type: nodeType,
position: [250, yPosition],
parameters: this.mapStepParameters(step)
});
// Add connection from previous node
if (!connections[prevNodeId]) {
connections[prevNodeId] = { main: [[]] };
}
connections[prevNodeId].main[0].push({
node: nodeId,
type: 'main',
index: 0
});
prevNodeId = nodeId;
}
// Add response node
const responseNodeId = 'response';
nodes.push({
id: responseNodeId,
name: 'Response',
type: 'n8n-nodes-base.respondToWebhook',
position: [250, yPosition + 150],
parameters: {
respondWith: 'json',
responseBody: '={{ $json }}'
}
});
connections[prevNodeId] = {
main: [[{ node: responseNodeId, type: 'main', index: 0 }]]
};
return {
name,
nodes,
connections,
settings: {
executionOrder: 'v1'
}
};
}
/**
* Map PraisonAI step type to n8n node type
*/
private mapStepTypeToN8N(type: string): string {
const mapping: Record<string, string> = {
agent: 'n8n-nodes-base.httpRequest',
tool: 'n8n-nodes-base.function',
condition: 'n8n-nodes-base.if'
};
return mapping[type] || 'n8n-nodes-base.noOp';
}
/**
* Map step parameters to n8n format
*/
private mapStepParameters(step: { type: string; config?: Record<string, any> }): Record<string, any> {
switch (step.type) {
case 'agent':
return {
method: 'POST',
url: '={{ $env.PRAISONAI_API_URL }}/agent/execute',
sendBody: true,
bodyParameters: {
parameters: [
{ name: 'input', value: '={{ $json.input }}' }
]
},
...step.config
};
case 'tool':
return {
functionCode: `
// Tool execution
const input = $input.all();
// Add your tool logic here
return input;
`.trim(),
...step.config
};
case 'condition':
return {
conditions: {
boolean: [
{
value1: '={{ $json.success }}',
value2: true
}
]
},
...step.config
};
default:
return step.config || {};
}
}
/**
* Generate n8n workflow JSON string
*/
exportWorkflowJSON(
name: string,
steps: Array<{ name: string; type: 'agent' | 'tool' | 'condition'; config?: Record<string, any> }>
): string {
const workflow = this.exportWorkflow(name, steps);
return JSON.stringify(workflow, null, 2);
}
/**
* Import n8n workflow and convert to PraisonAI format
*/
importWorkflow(n8nWorkflow: N8NWorkflow): Array<{
name: string;
type: string;
config: Record<string, any>;
}> {
const steps: Array<{ name: string; type: string; config: Record<string, any> }> = [];
// Skip trigger and response nodes
const workflowNodes = n8nWorkflow.nodes.filter(
n => !n.type.includes('webhook') && !n.type.includes('respondToWebhook')
);
for (const node of workflowNodes) {
steps.push({
name: node.name,
type: this.mapN8NTypeToStep(node.type),
config: node.parameters
});
}
return steps;
}
/**
* Map n8n node type to PraisonAI step type
*/
private mapN8NTypeToStep(n8nType: string): string {
if (n8nType.includes('httpRequest')) return 'agent';
if (n8nType.includes('function') || n8nType.includes('code')) return 'tool';
if (n8nType.includes('if') || n8nType.includes('switch')) return 'condition';
return 'tool';
}
/**
* Create a webhook listener (for local development)
*/
async createWebhookListener(
port: number,
handler: (payload: N8NWebhookPayload) => Promise<any>
): Promise<{ close: () => void }> {
const http = await import('http');
const server = http.createServer(async (req, res) => {
if (req.method !== 'POST') {
res.writeHead(405);
res.end('Method not allowed');
return;
}
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', async () => {
try {
const payload = JSON.parse(body) as N8NWebhookPayload;
const result = await handler(payload);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} catch (error: any) {
res.writeHead(500);
res.end(JSON.stringify({ error: error.message }));
}
});
});
server.listen(port);
console.log(`N8N webhook listener started on port ${port}`);
return {
close: () => server.close()
};
}
}
/**
* Create an n8n integration instance
*/
export function createN8NIntegration(config?: N8NConfig): N8NIntegration {
return new N8NIntegration(config);
}
/**
* Quick function to trigger an n8n webhook
*/
export async function triggerN8NWebhook(
webhookId: string,
payload: Record<string, any>,
config?: N8NConfig
): Promise<{ success: boolean; response?: any; error?: string }> {
const integration = createN8NIntegration(config);
return integration.triggerWebhook(webhookId, payload);
}
@@ -0,0 +1,383 @@
/**
* Repo Map - Repository structure visualization and symbol extraction
*/
export interface RepoMapConfig {
rootPath: string;
maxDepth?: number;
ignorePatterns?: string[];
includeSymbols?: boolean;
maxFiles?: number;
}
export interface FileInfo {
path: string;
relativePath: string;
size: number;
isDirectory: boolean;
children?: FileInfo[];
symbols?: SymbolInfo[];
}
export interface SymbolInfo {
name: string;
type: 'function' | 'class' | 'method' | 'variable' | 'interface' | 'type' | 'export';
line: number;
exported?: boolean;
}
export interface RepoMapResult {
root: string;
tree: FileInfo;
totalFiles: number;
totalDirectories: number;
symbols: SymbolInfo[];
summary: string;
}
/**
* Default ignore patterns
*/
export const DEFAULT_IGNORE_PATTERNS = [
'node_modules',
'.git',
'dist',
'build',
'.next',
'.nuxt',
'coverage',
'.cache',
'__pycache__',
'.pytest_cache',
'venv',
'.venv',
'*.pyc',
'*.pyo',
'.DS_Store',
'Thumbs.db',
'*.log',
'*.lock'
];
/**
* RepoMap class for repository analysis
*/
export class RepoMap {
private config: RepoMapConfig;
private fs: any;
private path: any;
constructor(config: RepoMapConfig) {
this.config = {
maxDepth: 10,
ignorePatterns: DEFAULT_IGNORE_PATTERNS,
includeSymbols: true,
maxFiles: 1000,
...config
};
}
/**
* Generate repository map
*/
async generate(): Promise<RepoMapResult> {
this.fs = await import('fs/promises');
this.path = await import('path');
const tree = await this.scanDirectory(this.config.rootPath, 0);
const { files, directories } = this.countItems(tree);
const symbols = this.config.includeSymbols ? this.collectSymbols(tree) : [];
return {
root: this.config.rootPath,
tree,
totalFiles: files,
totalDirectories: directories,
symbols,
summary: this.generateSummary(tree, files, directories, symbols.length)
};
}
/**
* Scan a directory recursively
*/
private async scanDirectory(dirPath: string, depth: number): Promise<FileInfo> {
const relativePath = this.path.relative(this.config.rootPath, dirPath);
const stats = await this.fs.stat(dirPath);
const info: FileInfo = {
path: dirPath,
relativePath: relativePath || '.',
size: stats.size,
isDirectory: stats.isDirectory()
};
if (!stats.isDirectory() || depth >= (this.config.maxDepth || 10)) {
return info;
}
const entries = await this.fs.readdir(dirPath, { withFileTypes: true });
info.children = [];
for (const entry of entries) {
if (this.shouldIgnore(entry.name)) continue;
const entryPath = this.path.join(dirPath, entry.name);
if (entry.isDirectory()) {
const child = await this.scanDirectory(entryPath, depth + 1);
info.children.push(child);
} else {
const fileStats = await this.fs.stat(entryPath).catch(() => null);
if (!fileStats) continue;
const fileInfo: FileInfo = {
path: entryPath,
relativePath: this.path.relative(this.config.rootPath, entryPath),
size: fileStats.size,
isDirectory: false
};
if (this.config.includeSymbols && this.isCodeFile(entry.name)) {
fileInfo.symbols = await this.extractSymbols(entryPath);
}
info.children.push(fileInfo);
}
}
// Sort: directories first, then files alphabetically
info.children.sort((a, b) => {
if (a.isDirectory !== b.isDirectory) {
return a.isDirectory ? -1 : 1;
}
return a.relativePath.localeCompare(b.relativePath);
});
return info;
}
/**
* Check if path should be ignored
*/
private shouldIgnore(name: string): boolean {
for (const pattern of this.config.ignorePatterns || []) {
if (pattern.startsWith('*')) {
if (name.endsWith(pattern.slice(1))) return true;
} else if (name === pattern) {
return true;
}
}
return false;
}
/**
* Check if file is a code file
*/
private isCodeFile(name: string): boolean {
const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.rb', '.php'];
return codeExtensions.some(ext => name.endsWith(ext));
}
/**
* Extract symbols from a file
*/
private async extractSymbols(filePath: string): Promise<SymbolInfo[]> {
try {
const content = await this.fs.readFile(filePath, 'utf-8');
const ext = this.path.extname(filePath);
switch (ext) {
case '.ts':
case '.tsx':
case '.js':
case '.jsx':
return this.extractJSSymbols(content);
case '.py':
return this.extractPythonSymbols(content);
default:
return [];
}
} catch {
return [];
}
}
/**
* Extract symbols from JavaScript/TypeScript
*/
private extractJSSymbols(content: string): SymbolInfo[] {
const symbols: SymbolInfo[] = [];
const lines = content.split('\n');
const patterns = [
{ regex: /^export\s+(?:async\s+)?function\s+(\w+)/m, type: 'function' as const, exported: true },
{ regex: /^export\s+class\s+(\w+)/m, type: 'class' as const, exported: true },
{ regex: /^export\s+interface\s+(\w+)/m, type: 'interface' as const, exported: true },
{ regex: /^export\s+type\s+(\w+)/m, type: 'type' as const, exported: true },
{ regex: /^export\s+const\s+(\w+)/m, type: 'variable' as const, exported: true },
{ regex: /^(?:async\s+)?function\s+(\w+)/m, type: 'function' as const },
{ regex: /^class\s+(\w+)/m, type: 'class' as const },
{ regex: /^interface\s+(\w+)/m, type: 'interface' as const },
{ regex: /^type\s+(\w+)/m, type: 'type' as const },
{ regex: /^const\s+(\w+)\s*=/m, type: 'variable' as const },
];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
for (const { regex, type, exported } of patterns) {
const match = line.match(regex);
if (match) {
symbols.push({
name: match[1],
type,
line: i + 1,
exported
});
break;
}
}
}
return symbols;
}
/**
* Extract symbols from Python
*/
private extractPythonSymbols(content: string): SymbolInfo[] {
const symbols: SymbolInfo[] = [];
const lines = content.split('\n');
const patterns = [
{ regex: /^def\s+(\w+)\s*\(/m, type: 'function' as const },
{ regex: /^async\s+def\s+(\w+)\s*\(/m, type: 'function' as const },
{ regex: /^class\s+(\w+)/m, type: 'class' as const },
];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Skip indented definitions (methods)
if (line.startsWith(' ') || line.startsWith('\t')) continue;
for (const { regex, type } of patterns) {
const match = line.match(regex);
if (match) {
symbols.push({
name: match[1],
type,
line: i + 1
});
break;
}
}
}
return symbols;
}
/**
* Count files and directories
*/
private countItems(tree: FileInfo): { files: number; directories: number } {
let files = 0;
let directories = 0;
const count = (node: FileInfo) => {
if (node.isDirectory) {
directories++;
for (const child of node.children || []) {
count(child);
}
} else {
files++;
}
};
count(tree);
return { files, directories };
}
/**
* Collect all symbols from tree
*/
private collectSymbols(tree: FileInfo): SymbolInfo[] {
const symbols: SymbolInfo[] = [];
const collect = (node: FileInfo) => {
if (node.symbols) {
symbols.push(...node.symbols);
}
for (const child of node.children || []) {
collect(child);
}
};
collect(tree);
return symbols;
}
/**
* Generate text summary
*/
private generateSummary(tree: FileInfo, files: number, directories: number, symbolCount: number): string {
const lines: string[] = [];
lines.push(`Repository: ${this.config.rootPath}`);
lines.push(`Files: ${files}, Directories: ${directories}, Symbols: ${symbolCount}`);
lines.push('');
lines.push(this.renderTree(tree, ''));
return lines.join('\n');
}
/**
* Render tree as text
*/
private renderTree(node: FileInfo, prefix: string): string {
const lines: string[] = [];
const name = this.path.basename(node.path);
if (node.isDirectory) {
lines.push(`${prefix}${name}/`);
const children = node.children || [];
for (let i = 0; i < children.length; i++) {
const isLast = i === children.length - 1;
const childPrefix = prefix + (isLast ? '└── ' : '├── ');
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
lines.push(this.renderTree(children[i], childPrefix).replace(childPrefix, ''));
// Add children lines with proper prefix
const childLines = this.renderTree(children[i], nextPrefix).split('\n').slice(1);
lines.push(...childLines);
}
} else {
const symbolCount = node.symbols?.length || 0;
const symbolSuffix = symbolCount > 0 ? ` (${symbolCount} symbols)` : '';
lines.push(`${prefix}${name}${symbolSuffix}`);
}
return lines.join('\n');
}
/**
* Get tree as simple text format
*/
async getTreeText(): Promise<string> {
const result = await this.generate();
return result.summary;
}
}
/**
* Create a repo map instance
*/
export function createRepoMap(config: RepoMapConfig): RepoMap {
return new RepoMap(config);
}
/**
* Quick function to get repo tree
*/
export async function getRepoTree(rootPath: string, maxDepth?: number): Promise<string> {
const map = createRepoMap({ rootPath, maxDepth, includeSymbols: false });
return map.getTreeText();
}
@@ -0,0 +1,334 @@
/**
* Sandbox Executor - Safe command execution with restrictions
*/
export type SandboxMode = 'disabled' | 'basic' | 'strict' | 'network-isolated';
export interface SandboxConfig {
mode: SandboxMode;
allowedCommands?: string[];
blockedCommands?: string[];
blockedPaths?: string[];
timeout?: number;
maxOutputSize?: number;
cwd?: string;
env?: Record<string, string>;
}
export interface ExecutionResult {
success: boolean;
stdout: string;
stderr: string;
exitCode: number;
duration: number;
truncated?: boolean;
}
/**
* Default blocked commands (dangerous operations)
*/
export const DEFAULT_BLOCKED_COMMANDS = [
'rm -rf /',
'rm -rf /*',
'rm -rf ~',
'rm -rf ~/*',
'mkfs',
'dd if=/dev/zero',
'dd if=/dev/random',
':(){ :|:& };:',
'chmod -R 777 /',
'chown -R',
'sudo rm',
'sudo dd',
'sudo mkfs',
'shutdown',
'reboot',
'halt',
'poweroff',
'init 0',
'init 6'
];
/**
* Default blocked paths
*/
export const DEFAULT_BLOCKED_PATHS = [
'/etc/passwd',
'/etc/shadow',
'/etc/sudoers',
'/root',
'~/.ssh',
'~/.gnupg',
'~/.aws',
'~/.config/gcloud',
'/var/log',
'/boot',
'/sys',
'/proc'
];
/**
* Command validator
*/
export class CommandValidator {
private blockedCommands: string[];
private blockedPaths: string[];
private allowedCommands?: string[];
constructor(config: Partial<SandboxConfig> = {}) {
this.blockedCommands = config.blockedCommands || DEFAULT_BLOCKED_COMMANDS;
this.blockedPaths = config.blockedPaths || DEFAULT_BLOCKED_PATHS;
this.allowedCommands = config.allowedCommands;
}
/**
* Validate a command
*/
validate(command: string): { valid: boolean; reason?: string } {
const normalized = command.toLowerCase().trim();
// Check allowlist first if specified
if (this.allowedCommands) {
const baseCmd = normalized.split(/\s+/)[0];
if (!this.allowedCommands.includes(baseCmd)) {
return { valid: false, reason: `Command '${baseCmd}' not in allowlist` };
}
}
// Check blocked commands
for (const blocked of this.blockedCommands) {
if (normalized.includes(blocked.toLowerCase())) {
return { valid: false, reason: `Blocked command pattern: ${blocked}` };
}
}
// Check blocked paths
for (const blockedPath of this.blockedPaths) {
const expandedPath = blockedPath.replace('~', process.env.HOME || '');
if (normalized.includes(expandedPath.toLowerCase())) {
return { valid: false, reason: `Blocked path: ${blockedPath}` };
}
}
// Check for shell injection patterns
const dangerousPatterns = [
/;\s*rm\s/i,
/\|\s*rm\s/i,
/`.*`/,
/\$\(.*\)/,
/>\s*\/dev\/sd/i,
/>\s*\/dev\/null.*2>&1.*&/i
];
for (const pattern of dangerousPatterns) {
if (pattern.test(command)) {
return { valid: false, reason: 'Potentially dangerous pattern detected' };
}
}
return { valid: true };
}
}
/**
* Sandbox Executor class
*/
export class SandboxExecutor {
private config: SandboxConfig;
private validator: CommandValidator;
constructor(config: Partial<SandboxConfig> = {}) {
this.config = {
mode: 'basic',
timeout: 30000,
maxOutputSize: 1024 * 1024, // 1MB
cwd: process.cwd(),
...config
};
this.validator = new CommandValidator(this.config);
}
/**
* Execute a command in the sandbox
*/
async execute(command: string): Promise<ExecutionResult> {
const startTime = Date.now();
// Disabled mode - no execution
if (this.config.mode === 'disabled') {
return {
success: false,
stdout: '',
stderr: 'Sandbox execution is disabled',
exitCode: 1,
duration: 0
};
}
// Validate command
const validation = this.validator.validate(command);
if (!validation.valid) {
return {
success: false,
stdout: '',
stderr: `Command rejected: ${validation.reason}`,
exitCode: 1,
duration: 0
};
}
try {
const result = await this.spawn(command);
return {
...result,
duration: Date.now() - startTime
};
} catch (error: any) {
return {
success: false,
stdout: '',
stderr: error.message,
exitCode: 1,
duration: Date.now() - startTime
};
}
}
/**
* Spawn the command
*/
private async spawn(command: string): Promise<Omit<ExecutionResult, 'duration'>> {
const { spawn } = await import('child_process');
return new Promise((resolve) => {
const env = this.buildEnv();
const proc = spawn('sh', ['-c', command], {
cwd: this.config.cwd,
env,
timeout: this.config.timeout,
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
let truncated = false;
const maxSize = this.config.maxOutputSize || 1024 * 1024;
proc.stdout?.on('data', (data) => {
if (stdout.length < maxSize) {
stdout += data.toString();
if (stdout.length > maxSize) {
stdout = stdout.slice(0, maxSize);
truncated = true;
}
}
});
proc.stderr?.on('data', (data) => {
if (stderr.length < maxSize) {
stderr += data.toString();
if (stderr.length > maxSize) {
stderr = stderr.slice(0, maxSize);
truncated = true;
}
}
});
proc.on('close', (code) => {
resolve({
success: code === 0,
stdout,
stderr,
exitCode: code || 0,
truncated
});
});
proc.on('error', (error) => {
resolve({
success: false,
stdout,
stderr: error.message,
exitCode: 1
});
});
});
}
/**
* Build environment variables based on sandbox mode
*/
private buildEnv(): Record<string, string> {
const baseEnv = { ...process.env, ...this.config.env };
switch (this.config.mode) {
case 'strict':
// Minimal environment
return {
PATH: '/usr/local/bin:/usr/bin:/bin',
HOME: process.env.HOME || '',
USER: process.env.USER || '',
SHELL: '/bin/sh',
TERM: 'dumb',
...this.config.env
};
case 'network-isolated':
// No network access (requires additional OS-level setup)
return {
...baseEnv,
http_proxy: 'http://localhost:0',
https_proxy: 'http://localhost:0',
HTTP_PROXY: 'http://localhost:0',
HTTPS_PROXY: 'http://localhost:0',
no_proxy: '',
NO_PROXY: ''
};
case 'basic':
default:
return baseEnv as Record<string, string>;
}
}
/**
* Check if a command would be allowed
*/
wouldAllow(command: string): { allowed: boolean; reason?: string } {
if (this.config.mode === 'disabled') {
return { allowed: false, reason: 'Sandbox execution is disabled' };
}
const validation = this.validator.validate(command);
return { allowed: validation.valid, reason: validation.reason };
}
/**
* Get current sandbox mode
*/
getMode(): SandboxMode {
return this.config.mode;
}
/**
* Update sandbox mode
*/
setMode(mode: SandboxMode): void {
this.config.mode = mode;
}
}
/**
* Create a sandbox executor
*/
export function createSandboxExecutor(config?: Partial<SandboxConfig>): SandboxExecutor {
return new SandboxExecutor(config);
}
/**
* Quick execute with default sandbox
*/
export async function sandboxExec(command: string, config?: Partial<SandboxConfig>): Promise<ExecutionResult> {
const executor = createSandboxExecutor(config);
return executor.execute(command);
}
@@ -0,0 +1,360 @@
/**
* Agent Scheduler - Cron-like scheduling for agent tasks
*/
export interface ScheduleConfig {
id?: string;
name: string;
cron?: string;
interval?: number; // milliseconds
task: () => Promise<any>;
enabled?: boolean;
maxRuns?: number;
onComplete?: (result: any) => void;
onError?: (error: Error) => void;
}
export interface ScheduledTask {
id: string;
name: string;
cron?: string;
interval?: number;
enabled: boolean;
lastRun?: Date;
nextRun?: Date;
runCount: number;
maxRuns?: number;
status: 'idle' | 'running' | 'completed' | 'error';
lastError?: string;
}
export interface SchedulerStats {
totalTasks: number;
activeTasks: number;
totalRuns: number;
errors: number;
}
/**
* Parse cron expression (simplified: minute hour day month weekday)
*/
function parseCron(cron: string): { minute: number[]; hour: number[]; day: number[]; month: number[]; weekday: number[] } {
const parts = cron.split(/\s+/);
if (parts.length !== 5) {
throw new Error('Invalid cron expression. Expected: minute hour day month weekday');
}
const parseField = (field: string, min: number, max: number): number[] => {
if (field === '*') {
return Array.from({ length: max - min + 1 }, (_, i) => min + i);
}
if (field.includes('/')) {
const [, step] = field.split('/');
const stepNum = parseInt(step);
return Array.from({ length: Math.ceil((max - min + 1) / stepNum) }, (_, i) => min + i * stepNum);
}
if (field.includes(',')) {
return field.split(',').map(n => parseInt(n));
}
if (field.includes('-')) {
const [start, end] = field.split('-').map(n => parseInt(n));
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
return [parseInt(field)];
};
return {
minute: parseField(parts[0], 0, 59),
hour: parseField(parts[1], 0, 23),
day: parseField(parts[2], 1, 31),
month: parseField(parts[3], 1, 12),
weekday: parseField(parts[4], 0, 6)
};
}
/**
* Get next run time from cron expression
*/
function getNextCronRun(cron: string, from: Date = new Date()): Date {
const parsed = parseCron(cron);
const next = new Date(from);
next.setSeconds(0);
next.setMilliseconds(0);
next.setMinutes(next.getMinutes() + 1);
for (let i = 0; i < 366 * 24 * 60; i++) { // Max 1 year search
const minute = next.getMinutes();
const hour = next.getHours();
const day = next.getDate();
const month = next.getMonth() + 1;
const weekday = next.getDay();
if (
parsed.minute.includes(minute) &&
parsed.hour.includes(hour) &&
parsed.day.includes(day) &&
parsed.month.includes(month) &&
parsed.weekday.includes(weekday)
) {
return next;
}
next.setMinutes(next.getMinutes() + 1);
}
throw new Error('Could not find next run time');
}
/**
* Scheduler class
*/
export class Scheduler {
private tasks: Map<string, { config: ScheduleConfig; task: ScheduledTask; timer?: NodeJS.Timeout }> = new Map();
private running: boolean = false;
private stats: SchedulerStats = {
totalTasks: 0,
activeTasks: 0,
totalRuns: 0,
errors: 0
};
/**
* Add a scheduled task
*/
add(config: ScheduleConfig): string {
const id = config.id || `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const task: ScheduledTask = {
id,
name: config.name,
cron: config.cron,
interval: config.interval,
enabled: config.enabled ?? true,
runCount: 0,
maxRuns: config.maxRuns,
status: 'idle'
};
if (config.cron) {
task.nextRun = getNextCronRun(config.cron);
} else if (config.interval) {
task.nextRun = new Date(Date.now() + config.interval);
}
this.tasks.set(id, { config, task });
this.stats.totalTasks++;
if (this.running && task.enabled) {
this.scheduleTask(id);
}
return id;
}
/**
* Remove a task
*/
remove(id: string): boolean {
const entry = this.tasks.get(id);
if (!entry) return false;
if (entry.timer) {
clearTimeout(entry.timer);
}
this.tasks.delete(id);
this.stats.totalTasks--;
return true;
}
/**
* Enable a task
*/
enable(id: string): boolean {
const entry = this.tasks.get(id);
if (!entry) return false;
entry.task.enabled = true;
if (this.running) {
this.scheduleTask(id);
}
return true;
}
/**
* Disable a task
*/
disable(id: string): boolean {
const entry = this.tasks.get(id);
if (!entry) return false;
entry.task.enabled = false;
if (entry.timer) {
clearTimeout(entry.timer);
entry.timer = undefined;
}
return true;
}
/**
* Start the scheduler
*/
start(): void {
if (this.running) return;
this.running = true;
for (const [id, entry] of this.tasks) {
if (entry.task.enabled) {
this.scheduleTask(id);
}
}
}
/**
* Stop the scheduler
*/
stop(): void {
this.running = false;
for (const entry of this.tasks.values()) {
if (entry.timer) {
clearTimeout(entry.timer);
entry.timer = undefined;
}
}
}
/**
* Schedule a task's next run
*/
private scheduleTask(id: string): void {
const entry = this.tasks.get(id);
if (!entry || !entry.task.enabled || !this.running) return;
// Check if max runs reached
if (entry.task.maxRuns && entry.task.runCount >= entry.task.maxRuns) {
entry.task.status = 'completed';
return;
}
let delay: number;
if (entry.config.cron) {
const nextRun = getNextCronRun(entry.config.cron);
entry.task.nextRun = nextRun;
delay = nextRun.getTime() - Date.now();
} else if (entry.config.interval) {
entry.task.nextRun = new Date(Date.now() + entry.config.interval);
delay = entry.config.interval;
} else {
return;
}
entry.timer = setTimeout(() => this.runTask(id), delay);
this.stats.activeTasks = Array.from(this.tasks.values()).filter(e => e.timer).length;
}
/**
* Run a task
*/
private async runTask(id: string): Promise<void> {
const entry = this.tasks.get(id);
if (!entry) return;
entry.task.status = 'running';
entry.task.lastRun = new Date();
try {
const result = await entry.config.task();
entry.task.runCount++;
entry.task.status = 'idle';
entry.task.lastError = undefined;
this.stats.totalRuns++;
entry.config.onComplete?.(result);
} catch (error: any) {
entry.task.status = 'error';
entry.task.lastError = error.message;
this.stats.errors++;
entry.config.onError?.(error);
}
// Schedule next run
this.scheduleTask(id);
}
/**
* Run a task immediately
*/
async runNow(id: string): Promise<any> {
const entry = this.tasks.get(id);
if (!entry) throw new Error(`Task ${id} not found`);
entry.task.status = 'running';
entry.task.lastRun = new Date();
try {
const result = await entry.config.task();
entry.task.runCount++;
entry.task.status = 'idle';
this.stats.totalRuns++;
return result;
} catch (error: any) {
entry.task.status = 'error';
entry.task.lastError = error.message;
this.stats.errors++;
throw error;
}
}
/**
* Get task info
*/
getTask(id: string): ScheduledTask | undefined {
return this.tasks.get(id)?.task;
}
/**
* Get all tasks
*/
getAllTasks(): ScheduledTask[] {
return Array.from(this.tasks.values()).map(e => e.task);
}
/**
* Get scheduler stats
*/
getStats(): SchedulerStats {
return { ...this.stats };
}
/**
* Check if scheduler is running
*/
isRunning(): boolean {
return this.running;
}
}
/**
* Create a scheduler instance
*/
export function createScheduler(): Scheduler {
return new Scheduler();
}
/**
* Helper to create common cron expressions
*/
export const cronExpressions = {
everyMinute: '* * * * *',
every5Minutes: '*/5 * * * *',
every15Minutes: '*/15 * * * *',
everyHour: '0 * * * *',
everyDay: '0 0 * * *',
everyWeek: '0 0 * * 0',
everyMonth: '0 0 1 * *',
weekdays: '0 9 * * 1-5',
weekends: '0 10 * * 0,6'
};
@@ -0,0 +1,372 @@
/**
* Slash Commands - Interactive command system for CLI
* Supports /help, /cost, /clear, /model, /tokens, /plan, /undo, /diff, /commit, /exit, /settings, /map
*/
export interface SlashCommand {
name: string;
aliases?: string[];
description: string;
usage?: string;
execute: (args: string[], context: SlashCommandContext) => Promise<SlashCommandResult>;
}
export interface SlashCommandContext {
sessionId?: string;
model?: string;
tokenCount?: number;
costTracker?: CostTracker;
history?: string[];
settings?: Record<string, any>;
onOutput?: (message: string) => void;
}
export interface SlashCommandResult {
success: boolean;
message?: string;
data?: any;
shouldExit?: boolean;
}
export interface CostTracker {
totalTokens: number;
totalCost: number;
requests: number;
addUsage(model: string, inputTokens: number, outputTokens: number, latencyMs?: number): any;
reset(): void;
getSummary(): string;
}
/**
* Built-in slash commands
*/
const BUILTIN_COMMANDS: SlashCommand[] = [
{
name: 'help',
aliases: ['h', '?'],
description: 'Show available commands',
execute: async (_args, context) => {
const commands = getRegistry().getAll();
const lines = ['Available commands:', ''];
for (const cmd of commands) {
const aliases = cmd.aliases ? ` (${cmd.aliases.map(a => '/' + a).join(', ')})` : '';
lines.push(` /${cmd.name}${aliases} - ${cmd.description}`);
if (cmd.usage) {
lines.push(` Usage: ${cmd.usage}`);
}
}
context.onOutput?.(lines.join('\n'));
return { success: true, message: lines.join('\n') };
}
},
{
name: 'cost',
aliases: ['c'],
description: 'Show token usage and cost',
execute: async (_args, context) => {
if (!context.costTracker) {
return { success: false, message: 'Cost tracking not enabled' };
}
const summary = context.costTracker.getSummary();
context.onOutput?.(summary);
return { success: true, message: summary };
}
},
{
name: 'clear',
description: 'Clear conversation history',
execute: async (_args, context) => {
if (context.history) {
context.history.length = 0;
}
context.onOutput?.('Conversation cleared');
return { success: true, message: 'Conversation cleared' };
}
},
{
name: 'model',
aliases: ['m'],
description: 'Show or change the current model',
usage: '/model [model-name]',
execute: async (args, context) => {
if (args.length === 0) {
const msg = `Current model: ${context.model || 'default'}`;
context.onOutput?.(msg);
return { success: true, message: msg, data: { model: context.model } };
}
const newModel = args[0];
context.model = newModel;
const msg = `Model changed to: ${newModel}`;
context.onOutput?.(msg);
return { success: true, message: msg, data: { model: newModel } };
}
},
{
name: 'tokens',
aliases: ['t'],
description: 'Show token count for current session',
execute: async (_args, context) => {
const count = context.tokenCount || 0;
const msg = `Token count: ${count}`;
context.onOutput?.(msg);
return { success: true, message: msg, data: { tokens: count } };
}
},
{
name: 'plan',
aliases: ['p'],
description: 'Show or manage task plan',
usage: '/plan [add|remove|clear] [task]',
execute: async (args, context) => {
const plans = context.settings?.plans || [];
if (args.length === 0) {
if (plans.length === 0) {
context.onOutput?.('No plans set');
return { success: true, message: 'No plans set' };
}
const msg = ['Current plan:', ...plans.map((p: string, i: number) => ` ${i + 1}. ${p}`)].join('\n');
context.onOutput?.(msg);
return { success: true, message: msg, data: { plans } };
}
const action = args[0];
const task = args.slice(1).join(' ');
if (action === 'add' && task) {
plans.push(task);
context.settings = { ...context.settings, plans };
context.onOutput?.(`Added: ${task}`);
return { success: true, message: `Added: ${task}` };
}
if (action === 'remove' && task) {
const index = parseInt(task) - 1;
if (index >= 0 && index < plans.length) {
const removed = plans.splice(index, 1)[0];
context.settings = { ...context.settings, plans };
context.onOutput?.(`Removed: ${removed}`);
return { success: true, message: `Removed: ${removed}` };
}
}
if (action === 'clear') {
context.settings = { ...context.settings, plans: [] };
context.onOutput?.('Plans cleared');
return { success: true, message: 'Plans cleared' };
}
return { success: false, message: 'Invalid plan command' };
}
},
{
name: 'undo',
aliases: ['u'],
description: 'Undo last action',
execute: async (_args, context) => {
if (context.history && context.history.length > 0) {
const removed = context.history.pop();
context.onOutput?.(`Undone: ${removed?.substring(0, 50)}...`);
return { success: true, message: 'Last action undone' };
}
return { success: false, message: 'Nothing to undo' };
}
},
{
name: 'diff',
aliases: ['d'],
description: 'Show pending changes',
execute: async (_args, context) => {
const diff = context.settings?.pendingDiff || 'No pending changes';
context.onOutput?.(diff);
return { success: true, message: diff };
}
},
{
name: 'commit',
description: 'Commit pending changes',
execute: async (args, context) => {
const message = args.join(' ') || 'Auto-commit';
context.onOutput?.(`Committed with message: ${message}`);
context.settings = { ...context.settings, pendingDiff: null };
return { success: true, message: `Committed: ${message}` };
}
},
{
name: 'exit',
aliases: ['quit', 'q'],
description: 'Exit the session',
execute: async (_args, context) => {
context.onOutput?.('Goodbye!');
return { success: true, message: 'Exiting', shouldExit: true };
}
},
{
name: 'settings',
aliases: ['s'],
description: 'Show or modify settings',
usage: '/settings [key] [value]',
execute: async (args, context) => {
if (args.length === 0) {
const settings = JSON.stringify(context.settings || {}, null, 2);
context.onOutput?.(settings);
return { success: true, message: settings, data: context.settings };
}
if (args.length === 1) {
const value = context.settings?.[args[0]];
const msg = `${args[0]}: ${JSON.stringify(value)}`;
context.onOutput?.(msg);
return { success: true, message: msg, data: { [args[0]]: value } };
}
const [key, ...valueParts] = args;
let value: any = valueParts.join(' ');
try {
value = JSON.parse(value);
} catch {
// Keep as string
}
context.settings = { ...context.settings, [key]: value };
context.onOutput?.(`Set ${key} = ${JSON.stringify(value)}`);
return { success: true, message: `Set ${key}` };
}
},
{
name: 'map',
description: 'Show repository map',
execute: async (_args, context) => {
const map = context.settings?.repoMap || 'Repository map not available. Run /map refresh to generate.';
context.onOutput?.(map);
return { success: true, message: map };
}
}
];
/**
* Slash Command Registry
*/
class SlashCommandRegistry {
private commands: Map<string, SlashCommand> = new Map();
private aliases: Map<string, string> = new Map();
constructor() {
// Register built-in commands
for (const cmd of BUILTIN_COMMANDS) {
this.register(cmd);
}
}
register(command: SlashCommand): void {
this.commands.set(command.name, command);
if (command.aliases) {
for (const alias of command.aliases) {
this.aliases.set(alias, command.name);
}
}
}
get(name: string): SlashCommand | undefined {
const resolved = this.aliases.get(name) || name;
return this.commands.get(resolved);
}
getAll(): SlashCommand[] {
return Array.from(this.commands.values());
}
has(name: string): boolean {
return this.commands.has(name) || this.aliases.has(name);
}
}
let registry: SlashCommandRegistry | null = null;
export function getRegistry(): SlashCommandRegistry {
if (!registry) {
registry = new SlashCommandRegistry();
}
return registry;
}
export function registerCommand(command: SlashCommand): void {
getRegistry().register(command);
}
/**
* Parse slash command from input
*/
export function parseSlashCommand(input: string): { command: string; args: string[] } | null {
const trimmed = input.trim();
if (!trimmed.startsWith('/')) {
return null;
}
const parts = trimmed.slice(1).split(/\s+/);
const command = parts[0].toLowerCase();
const args = parts.slice(1);
return { command, args };
}
/**
* Execute a slash command
*/
export async function executeSlashCommand(
input: string,
context: SlashCommandContext
): Promise<SlashCommandResult> {
const parsed = parseSlashCommand(input);
if (!parsed) {
return { success: false, message: 'Not a slash command' };
}
const command = getRegistry().get(parsed.command);
if (!command) {
return { success: false, message: `Unknown command: /${parsed.command}` };
}
return command.execute(parsed.args, context);
}
/**
* Check if input is a slash command
*/
export function isSlashCommand(input: string): boolean {
return input.trim().startsWith('/');
}
/**
* Slash Command Handler for CLI integration
*/
export class SlashCommandHandler {
private context: SlashCommandContext;
constructor(initialContext: Partial<SlashCommandContext> = {}) {
this.context = {
history: [],
settings: {},
...initialContext
};
}
async handle(input: string): Promise<SlashCommandResult> {
return executeSlashCommand(input, this.context);
}
isCommand(input: string): boolean {
return isSlashCommand(input);
}
getContext(): SlashCommandContext {
return this.context;
}
updateContext(updates: Partial<SlashCommandContext>): void {
this.context = { ...this.context, ...updates };
}
registerCommand(command: SlashCommand): void {
registerCommand(command);
}
}
export function createSlashCommandHandler(context?: Partial<SlashCommandContext>): SlashCommandHandler {
return new SlashCommandHandler(context);
}
+147 -136
View File
@@ -1,163 +1,174 @@
#!/usr/bin/env node
/**
* PraisonAI TypeScript CLI
* Implements CLI Spec v1.0.0
*
* Binary: praisonai-ts
* Package: praisonai (npm)
*/
import { createProvider, isProviderAvailable, getAvailableProviders } from '../llm/providers';
import { Session } from '../session';
import { tool, ToolRegistry } from '../tools/decorator';
import { validateCommand, COMMANDS, GLOBAL_FLAGS, EXIT_CODES, CLI_SPEC_VERSION } from './spec/cli-spec';
import { exitInvalidArgs, exitError } from './runtime/exit';
import { outputJson, formatError } from './output/json';
import { ERROR_CODES, normalizeError } from './output/errors';
interface CLIOptions {
model?: string;
stream?: boolean;
verbose?: boolean;
sessionId?: string;
export interface ParsedArgs {
command: string;
subcommand?: string;
args: string[];
options: Record<string, unknown>;
}
async function chat(prompt: string, options: CLIOptions = {}): Promise<string> {
const model = options.model || process.env.PRAISONAI_MODEL || 'openai/gpt-4o-mini';
const provider = createProvider(model);
const session = new Session({ id: options.sessionId });
session.addMessage({ role: 'user', content: prompt });
/**
* Parse command line arguments according to CLI spec
*/
function parseArgs(argv: string[]): ParsedArgs {
const result: ParsedArgs = {
command: 'help',
args: [],
options: {}
};
if (options.stream) {
let fullText = '';
const stream = await provider.streamText({
messages: [{ role: 'user', content: prompt }],
onToken: (token) => {
process.stdout.write(token);
fullText += token;
}
});
for await (const chunk of stream) {
// Stream is consumed by onToken
}
console.log(); // New line after streaming
return fullText;
} else {
const result = await provider.generateText({
messages: [{ role: 'user', content: prompt }]
});
console.log(result.text);
return result.text;
}
}
async function listProviders(): Promise<void> {
console.log('Available Providers:');
const available = getAvailableProviders();
const providers = ['openai', 'anthropic', 'google'];
for (const p of providers) {
const status = available.includes(p) ? '✅' : '❌';
console.log(` ${status} ${p}`);
}
}
async function version(): Promise<void> {
const pkg = require('../../package.json');
console.log(`praisonai v${pkg.version}`);
}
async function help(): Promise<void> {
console.log(`
PraisonAI TypeScript CLI
Usage:
praisonai-ts chat <prompt> Chat with an AI agent
praisonai-ts providers List available providers
praisonai-ts version Show version
praisonai-ts help Show this help
Options:
--model, -m <model> Model to use (e.g., openai/gpt-4o-mini)
--stream, -s Enable streaming output
--verbose, -v Verbose output
--session <id> Session ID for conversation continuity
Environment Variables:
OPENAI_API_KEY OpenAI API key
ANTHROPIC_API_KEY Anthropic API key
GOOGLE_API_KEY Google API key
PRAISONAI_MODEL Default model
Examples:
praisonai-ts chat "Hello, how are you?"
praisonai-ts chat "Write a poem" --stream
praisonai-ts chat "Explain AI" -m anthropic/claude-sonnet-4-20250514
praisonai-ts providers
`);
}
function parseArgs(args: string[]): { command: string; prompt: string; options: CLIOptions } {
const options: CLIOptions = {};
let command = 'help';
let prompt = '';
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg === '--model' || arg === '-m') {
options.model = args[++i];
} else if (arg === '--stream' || arg === '-s') {
options.stream = true;
} else if (arg === '--verbose' || arg === '-v') {
options.verbose = true;
} else if (arg === '--session') {
options.sessionId = args[++i];
} else if (!arg.startsWith('-')) {
if (!command || command === 'help') {
command = arg;
let foundCommand = false;
while (i < argv.length) {
const arg = argv[i];
if (arg.startsWith('--')) {
// Long flag
const flagName = arg.slice(2);
const nextArg = argv[i + 1];
// Check if it's a boolean flag or has a value
if (!nextArg || nextArg.startsWith('-')) {
result.options[flagName] = true;
} else {
prompt = arg;
result.options[flagName] = nextArg;
i++;
}
} else if (arg.startsWith('-') && arg.length === 2) {
// Short flag
const shortFlag = arg.slice(1);
const nextArg = argv[i + 1];
// Find the corresponding long flag name from global flags and command-specific flags
const globalFlag = GLOBAL_FLAGS.find(f => f.short === shortFlag);
// Also check command-specific flags (e.g., -m for model in chat command)
const commandShortFlags: Record<string, string> = {
'm': 'model',
's': 'stream',
'a': 'agent',
't': 'tools'
};
const flagName = globalFlag?.name || commandShortFlags[shortFlag] || shortFlag;
if (!nextArg || nextArg.startsWith('-')) {
result.options[flagName] = true;
} else {
result.options[flagName] = nextArg;
i++;
}
} else if (!foundCommand) {
// First non-flag argument is the command
result.command = arg;
foundCommand = true;
} else if (!result.subcommand && COMMANDS[result.command]?.subcommands) {
// Check if this is a subcommand
const cmdSpec = COMMANDS[result.command];
if (cmdSpec.subcommands && arg in cmdSpec.subcommands) {
result.subcommand = arg;
} else {
result.args.push(arg);
}
} else {
// Positional argument
result.args.push(arg);
}
i++;
}
return { command, prompt, options };
// Handle --json shorthand
if (result.options.json) {
result.options.output = 'json';
}
return result;
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
const { command, prompt, options } = parseArgs(args);
/**
* Load and execute a command (lazy loading)
*/
async function executeCommand(parsed: ParsedArgs): Promise<void> {
const { command, subcommand, args, options } = parsed;
// Validate command exists
if (!validateCommand(command)) {
const isJson = options.output === 'json' || options.json;
if (isJson) {
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown command: ${command}`));
} else {
console.error(`Error: Unknown command '${command}'`);
console.error('Run "praisonai-ts help" for available commands');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
// Lazy load the command module
try {
switch (command) {
case 'chat':
if (!prompt) {
console.error('Error: Please provide a prompt');
process.exit(1);
}
await chat(prompt, options);
break;
case 'providers':
await listProviders();
break;
case 'version':
await version();
break;
case 'help':
default:
await help();
break;
const commandModule = await import(`./commands/${command}`);
// Pass subcommand as first arg if present
const commandArgs = subcommand ? [subcommand, ...args] : args;
await commandModule.execute(commandArgs, options);
} catch (error: unknown) {
const cliError = normalizeError(error);
const isJson = options.output === 'json' || options.json;
if (isJson) {
outputJson(formatError(cliError.code, cliError.message, cliError.details));
} else {
console.error(`Error: ${cliError.message}`);
if (options.verbose && error instanceof Error && error.stack) {
console.error(error.stack);
}
}
} catch (error: any) {
console.error('Error:', error.message);
if (options.verbose) {
console.error(error.stack);
}
process.exit(1);
process.exit(cliError.exitCode);
}
}
/**
* Main CLI entry point
*/
async function main(): Promise<void> {
const argv = process.argv.slice(2);
// Fast path for version
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
const helpModule = await import('./commands/help');
await helpModule.execute([], {});
return;
}
if (argv[0] === '--version') {
const versionModule = await import('./commands/version');
await versionModule.execute([], {});
return;
}
const parsed = parseArgs(argv);
await executeCommand(parsed);
}
// Export for programmatic use
export { chat, listProviders, version, help, parseArgs };
export { parseArgs, executeCommand, CLI_SPEC_VERSION };
// Run CLI if executed directly
if (require.main === module) {
main();
main().catch((error) => {
console.error('Fatal error:', error.message);
process.exit(EXIT_CODES.RUNTIME_ERROR);
});
}
+345
View File
@@ -111,6 +111,351 @@ export const COMMANDS: Record<string, Command> = {
}
}
},
memory: {
description: 'Manage agent memory',
subcommands: {
list: { description: 'List all memories' },
add: { description: 'Add a new memory', args: [{ name: 'content', type: 'string', required: true, position: 0 }] },
search: { description: 'Search memories', args: [{ name: 'query', type: 'string', required: true, position: 0 }] },
clear: { description: 'Clear all memories' }
}
},
session: {
description: 'Manage agent sessions',
subcommands: {
list: { description: 'List all sessions' },
create: { description: 'Create a new session', args: [{ name: 'id', type: 'string', required: false, position: 0 }] },
get: { description: 'Get session details', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
delete: { description: 'Delete a session', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
export: { description: 'Export session data', args: [{ name: 'id', type: 'string', required: true, position: 0 }] }
}
},
knowledge: {
description: 'Manage knowledge base',
subcommands: {
add: { description: 'Add knowledge from file or text', args: [{ name: 'source', type: 'string', required: true, position: 0 }] },
search: { description: 'Search knowledge base', args: [{ name: 'query', type: 'string', required: true, position: 0 }] },
list: { description: 'List all knowledge entries' }
}
},
skills: {
description: 'Manage agent skills',
subcommands: {
list: { description: 'List loaded skills' },
discover: { description: 'Discover skills in directory', args: [{ name: 'path', type: 'string', required: false, position: 0 }] },
validate: { description: 'Validate a skill', args: [{ name: 'path', type: 'string', required: true, position: 0 }] },
info: { description: 'Show skill information', args: [{ name: 'name', type: 'string', required: true, position: 0 }] }
}
},
mcp: {
description: 'Model Context Protocol management',
subcommands: {
list: { description: 'List registered MCP servers' },
add: { description: 'Add an MCP server', args: [{ name: 'name', type: 'string', required: true, position: 0 }] },
remove: { description: 'Remove an MCP server', args: [{ name: 'name', type: 'string', required: true, position: 0 }] },
start: { description: 'Start an MCP server', args: [{ name: 'name', type: 'string', required: true, position: 0 }] },
stop: { description: 'Stop an MCP server', args: [{ name: 'name', type: 'string', required: true, position: 0 }] }
}
},
auto: {
description: 'Auto-generate agents from topic',
args: [{ name: 'topic', type: 'string', required: true, position: 0 }],
flags: [
{ name: 'pattern', type: 'string', description: 'Agent pattern (sequential, parallel, routing)' },
{ name: 'agents', type: 'integer', description: 'Number of agents to generate' }
]
},
image: {
description: 'Image generation and analysis',
subcommands: {
generate: { description: 'Generate image from text', args: [{ name: 'prompt', type: 'string', required: true, position: 0 }] },
analyze: { description: 'Analyze an image', args: [{ name: 'url', type: 'string', required: true, position: 0 }] }
},
flags: [
{ name: 'size', type: 'string', description: 'Image size' },
{ name: 'quality', type: 'string', description: 'Image quality' },
{ name: 'style', type: 'string', description: 'Image style' }
]
},
research: {
description: 'Deep research on a topic',
args: [{ name: 'query', type: 'string', required: true, position: 0 }],
flags: [
{ name: 'depth', type: 'integer', description: 'Research depth (iterations)' },
{ name: 'max-sources', type: 'integer', description: 'Maximum sources to use' }
]
},
guardrail: {
description: 'Content validation and safety',
subcommands: {
check: { description: 'Check content against guardrails', args: [{ name: 'content', type: 'string', required: true, position: 0 }] }
},
flags: [
{ name: 'criteria', type: 'string', description: 'Custom validation criteria' }
]
},
telemetry: {
description: 'Usage monitoring and analytics',
subcommands: {
status: { description: 'Show telemetry status' },
enable: { description: 'Enable telemetry' },
disable: { description: 'Disable telemetry' },
clear: { description: 'Clear telemetry data' },
export: { description: 'Export telemetry data' }
}
},
planning: {
description: 'Task planning and todo management',
subcommands: {
create: { description: 'Create a new plan', args: [{ name: 'name', type: 'string', required: true, position: 0 }] },
todo: { description: 'Manage todo items' }
}
},
'query-rewrite': {
description: 'Rewrite queries for better search results',
args: [{ name: 'query', type: 'string', required: true, position: 0 }],
flags: [
{ name: 'strategy', type: 'string', description: 'Rewrite strategy (expand, simplify, decompose, rephrase, auto)' }
]
},
'prompt-expand': {
description: 'Expand prompts with more detail',
args: [{ name: 'prompt', type: 'string', required: true, position: 0 }],
flags: [
{ name: 'strategy', type: 'string', description: 'Expand strategy (detail, context, examples, constraints, auto)' }
]
},
router: {
description: 'Route requests to appropriate agents',
subcommands: {
analyze: { description: 'Analyze input and suggest routing', args: [{ name: 'input', type: 'string', required: true, position: 0 }] }
}
},
context: {
description: 'Manage conversation context',
subcommands: {
chat: { description: 'Chat with context management', args: [{ name: 'message', type: 'string', required: true, position: 0 }] },
summarize: { description: 'Summarize context', args: [{ name: 'text', type: 'string', required: true, position: 0 }] }
},
flags: [
{ name: 'max-messages', type: 'integer', description: 'Maximum messages in context' }
]
},
handoff: {
description: 'Agent handoff management',
subcommands: {
info: { description: 'Show handoff feature information' }
}
},
vector: {
description: 'Vector store management',
subcommands: {
info: { description: 'Show vector store information' },
providers: { description: 'List available vector store providers' }
}
},
observability: {
description: 'Monitoring and tracing',
subcommands: {
info: { description: 'Show observability information' },
providers: { description: 'List available observability providers' }
}
},
voice: {
description: 'Text-to-speech and speech-to-text',
subcommands: {
info: { description: 'Show voice feature information' },
providers: { description: 'List available voice providers' }
}
},
reranker: {
description: 'Document reranking',
subcommands: {
info: { description: 'Show reranker information' },
providers: { description: 'List available reranker providers' }
}
},
'graph-rag': {
description: 'Graph-based retrieval augmented generation',
subcommands: {
info: { description: 'Show Graph RAG information' }
}
},
cache: {
description: 'Caching management',
subcommands: {
info: { description: 'Show cache information' },
providers: { description: 'List available cache providers' }
}
},
db: {
description: 'Database adapter management',
subcommands: {
info: { description: 'Show database adapter information' },
adapters: { description: 'List available database adapters' }
}
},
scheduler: {
description: 'Agent task scheduling',
subcommands: {
create: { description: 'Create a scheduled task', args: [{ name: 'name', type: 'string', required: true, position: 0 }] },
list: { description: 'List all scheduled tasks' },
run: { description: 'Run a task immediately', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
remove: { description: 'Remove a scheduled task', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
enable: { description: 'Enable a task', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
disable: { description: 'Disable a task', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
patterns: { description: 'Show common cron patterns' }
},
flags: [
{ name: 'cron', type: 'string', description: 'Cron expression' },
{ name: 'interval', type: 'integer', description: 'Interval in seconds' }
]
},
jobs: {
description: 'Background job queue management',
subcommands: {
add: { description: 'Add a new job', args: [{ name: 'name', type: 'string', required: true, position: 0 }] },
list: { description: 'List all jobs' },
get: { description: 'Get job details', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
cancel: { description: 'Cancel a pending job', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
retry: { description: 'Retry a failed job', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
process: { description: 'Process a job immediately', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
stats: { description: 'Show queue statistics' },
cleanup: { description: 'Clean up old jobs' }
},
flags: [
{ name: 'storage', type: 'string', description: 'Path to job storage file' },
{ name: 'priority', type: 'string', description: 'Job priority (low, normal, high, critical)' },
{ name: 'status', type: 'string', description: 'Filter by status' }
]
},
checkpoints: {
description: 'Session state checkpointing',
subcommands: {
create: { description: 'Create a checkpoint', args: [{ name: 'name', type: 'string', required: false, position: 0 }] },
list: { description: 'List all checkpoints' },
get: { description: 'Get checkpoint details', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
restore: { description: 'Restore from checkpoint', args: [{ name: 'id', type: 'string', required: false, position: 0 }] },
delete: { description: 'Delete a checkpoint', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
export: { description: 'Export checkpoint as JSON', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
import: { description: 'Import checkpoint from JSON', args: [{ name: 'json', type: 'string', required: true, position: 0 }] }
},
flags: [
{ name: 'storage', type: 'string', description: 'Path to checkpoint storage' },
{ name: 'state', type: 'string', description: 'JSON state to save' }
]
},
'fast-context': {
description: 'Fast context retrieval',
subcommands: {
query: { description: 'Query for relevant context', args: [{ name: 'query', type: 'string', required: true, position: 0 }] },
stats: { description: 'Show cache statistics' },
clear: { description: 'Clear cache and sources' }
},
flags: [
{ name: 'max-tokens', type: 'integer', description: 'Maximum tokens in context' },
{ name: 'sources', type: 'string', description: 'Comma-separated source texts' }
]
},
autonomy: {
description: 'Autonomy mode management',
subcommands: {
status: { description: 'Show current autonomy status' },
set: { description: 'Set autonomy mode', args: [{ name: 'mode', type: 'string', required: true, position: 0 }] },
policies: { description: 'Show policies for a mode', args: [{ name: 'mode', type: 'string', required: false, position: 0 }] },
reset: { description: 'Reset autonomy state' }
}
},
sandbox: {
description: 'Safe command execution',
subcommands: {
exec: { description: 'Execute a command in sandbox', args: [{ name: 'command', type: 'string', required: true, position: 0 }] },
check: { description: 'Check if command is allowed', args: [{ name: 'command', type: 'string', required: true, position: 0 }] },
mode: { description: 'List available sandbox modes' }
},
flags: [
{ name: 'mode', type: 'string', description: 'Sandbox mode (disabled, basic, strict, network-isolated)' },
{ name: 'timeout', type: 'integer', description: 'Execution timeout in ms' }
]
},
'repo-map': {
description: 'Repository structure visualization',
subcommands: {
tree: { description: 'Show repository tree', args: [{ name: 'path', type: 'string', required: false, position: 0 }] },
symbols: { description: 'Extract code symbols', args: [{ name: 'path', type: 'string', required: false, position: 0 }] }
},
flags: [
{ name: 'depth', type: 'integer', description: 'Maximum directory depth' },
{ name: 'symbols', type: 'boolean', description: 'Include symbols in tree' }
]
},
git: {
description: 'Git integration (read-only)',
subcommands: {
status: { description: 'Show repository status' },
diff: { description: 'Show changes' },
log: { description: 'Show recent commits' },
branches: { description: 'List branches' },
stash: { description: 'List stashes' }
},
flags: [
{ name: 'cwd', type: 'string', description: 'Working directory' },
{ name: 'staged', type: 'boolean', description: 'Show staged changes only' },
{ name: 'limit', type: 'integer', description: 'Number of commits to show' }
]
},
n8n: {
description: 'N8N workflow integration',
subcommands: {
trigger: { description: 'Trigger an N8N webhook', args: [{ name: 'webhookId', type: 'string', required: true, position: 0 }] },
export: { description: 'Export workflow to N8N format', args: [{ name: 'name', type: 'string', required: false, position: 0 }] }
},
flags: [
{ name: 'base-url', type: 'string', description: 'N8N base URL' },
{ name: 'api-key', type: 'string', description: 'N8N API key' },
{ name: 'steps', type: 'string', description: 'Workflow steps' }
]
},
'external-agents': {
description: 'External AI CLI tool integration',
subcommands: {
list: { description: 'List all external agents' },
check: { description: 'Check agent availability', args: [{ name: 'name', type: 'string', required: true, position: 0 }] },
run: { description: 'Run with external agent', args: [{ name: 'name', type: 'string', required: true, position: 0 }] }
},
flags: [
{ name: 'cwd', type: 'string', description: 'Working directory' }
]
},
flow: {
description: 'Workflow flow visualization',
subcommands: {
show: { description: 'Display workflow as text', args: [{ name: 'steps', type: 'string', required: false, position: 0 }] },
dot: { description: 'Export as DOT format', args: [{ name: 'steps', type: 'string', required: false, position: 0 }] }
},
flags: [
{ name: 'steps', type: 'string', description: 'Steps as JSON or comma-separated' },
{ name: 'boxes', type: 'boolean', description: 'Display as ASCII boxes' },
{ name: 'compact', type: 'boolean', description: 'Compact display mode' }
]
},
cost: {
description: 'Token usage and cost tracking',
subcommands: {
summary: { description: 'Show cost summary' },
add: { description: 'Add token usage', args: [{ name: 'model', type: 'string', required: true, position: 0 }] },
reset: { description: 'Reset cost tracker' },
pricing: { description: 'Show model pricing', args: [{ name: 'model', type: 'string', required: false, position: 0 }] }
}
},
interactive: {
description: 'Interactive TUI mode',
flags: [
{ name: 'model', short: 'm', type: 'string', description: 'Model to use' },
{ name: 'prompt', type: 'string', description: 'Custom prompt string' },
{ name: 'history', type: 'string', description: 'Path to history file' }
]
},
version: {
description: 'Show CLI version',
flags: []
+5
View File
@@ -0,0 +1,5 @@
/**
* CLI Spec exports
*/
export * from './cli-spec';
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff. Show more