refactor: streamline JSON formatting and enhance documentation clarity

- Consolidated JSON structure in `.mcp.json` and `.claude/mcp.json` for improved readability.
- Updated `CLAUDE.md` and other documentation files to enhance formatting consistency and clarity.
- Improved the organization of agent roles and initialization procedures in `AGENTS.md`, `BOOTSTRAP.md`, and `IDENTITY.md`.
- Enhanced user understanding and personalization strategies in `USER.md` and `MEMORY.md`.
- Refined tool generation scripts for better error handling and output formatting in `discover-tools.js` and `openClaw-formatter.js`.
This commit is contained in:
M3gA-Mind
2026-03-10 05:31:26 +05:30
parent 65baaaced5
commit 01c6fd9421
61 changed files with 956 additions and 1038 deletions
+1 -8
View File
@@ -1,8 +1 @@
{
"mcpServers": {
"alphaHuman": {
"type": "http",
"url": "https://alphahuman.readme.io/mcp"
}
}
}
{ "mcpServers": { "alphaHuman": { "type": "http", "url": "https://alphahuman.readme.io/mcp" } } }
+1 -8
View File
@@ -1,8 +1 @@
{
"mcpServers": {
"alphaHuman": {
"type": "http",
"url": "https://alphahuman.readme.io/mcp"
}
}
}
{ "mcpServers": { "alphaHuman": { "type": "http", "url": "https://alphahuman.readme.io/mcp" } } }
+22 -15
View File
@@ -213,15 +213,15 @@ Advanced JavaScript execution engine for skills using V8 (via deno_core):
Set in `.env` (Vite exposes `VITE_*` prefixed vars):
| Variable | Purpose |
| ---------------------------- | -------------------------------------------------- |
| `VITE_BACKEND_URL` | Backend API URL (default: `http://localhost:5005`) |
| `VITE_TELEGRAM_API_ID` | Telegram MTProto API ID |
| `VITE_TELEGRAM_API_HASH` | Telegram MTProto API hash |
| `VITE_TELEGRAM_BOT_USERNAME` | Telegram bot username |
| `VITE_TELEGRAM_BOT_ID` | Telegram bot numeric ID |
| `VITE_SENTRY_DSN` | Sentry DSN for error reporting (optional) |
| `VITE_DEBUG` | Debug mode flag |
| Variable | Purpose |
| ---------------------------- | ------------------------------------------------------------------- |
| `VITE_BACKEND_URL` | Backend API URL (default: `http://localhost:5005`) |
| `VITE_TELEGRAM_API_ID` | Telegram MTProto API ID |
| `VITE_TELEGRAM_API_HASH` | Telegram MTProto API hash |
| `VITE_TELEGRAM_BOT_USERNAME` | Telegram bot username |
| `VITE_TELEGRAM_BOT_ID` | Telegram bot numeric ID |
| `VITE_SENTRY_DSN` | Sentry DSN for error reporting (optional) |
| `VITE_DEBUG` | Debug mode flag |
| `ALPHAHUMAN_DAEMON_INTERNAL` | Force internal daemon mode (default: false, uses external services) |
Production defaults are in `src/utils/config.ts`.
@@ -254,16 +254,18 @@ loadAIConfig() → AIConfig // Combined SOUL + TOOLS configuration
```
**Caching Strategy:**
- Memory cache (immediate)
- localStorage cache (30min TTL)
- GitHub remote (latest)
- Bundled fallback (reliable)
**TODO**: Set up public AI configuration repository to eliminate 404 fallback errors
- Current: AI config loaders try GitHub URLs first (fail with 404), then fallback to bundled files
- Console shows: "Failed to load resource: the server responded with a status of 404"
- Affected: Settings → AI Configuration "Refresh Soul/Tools" buttons
- Files: `src/lib/ai/soul/loader.ts`, `src/lib/ai/tools/loader.ts`
- Current: AI config loaders try GitHub URLs first (fail with 404), then fallback to bundled files
- Console shows: "Failed to load resource: the server responded with a status of 404"
- Affected: Settings → AI Configuration "Refresh Soul/Tools" buttons
- Files: `src/lib/ai/soul/loader.ts`, `src/lib/ai/tools/loader.ts`
### Unified Injection System
@@ -272,15 +274,17 @@ Every user message automatically gets AI context injected:
```typescript
// Unified injection (recommended)
import { injectAll } from '../lib/ai/injector';
const injectedMessage = await injectAll(userMessage);
// Individual injections (for specific needs)
import { injectSoul, injectTools } from '../lib/ai/injector';
const injectedMessage = await injectAll(userMessage);
const soulMessage = await injectSoul(userMessage);
const toolsMessage = await injectTools(userMessage);
```
**Message Format:**
```
[PERSONA_CONTEXT]
I am AlphaHuman: that incredibly smart, funny friend who loves helping people get stuff done
@@ -310,12 +314,14 @@ yarn skills:build && yarn tools:generate && tsc && vite build
```
**Process:**
1. **Discovery**: Spawns Tauri runtime to call `runtime_all_tools()`
2. **Parsing**: Extracts tool definitions with JSON Schema
3. **Formatting**: Generates OpenClaw-compliant markdown
4. **Bundling**: Includes in app for AI context injection
**Generated Output:**
- Professional documentation with usage examples
- Environment-specific configurations
- Tool categorization by skill
@@ -335,6 +341,7 @@ All use the unified `injectAll()` function for consistency.
### Settings UI
View and manage AI configuration in **Settings → AI Configuration**:
- Live SOUL personality preview
- TOOLS statistics and categories
- Individual refresh buttons
+5 -1
View File
@@ -3,6 +3,7 @@
TODO: Define different agent roles and specializations within the AlphaHuman system.
This file should define:
- Primary AlphaHuman agent role
- Specialized sub-agents (if any)
- Agent collaboration patterns
@@ -12,18 +13,21 @@ This file should define:
## Example Structure:
### Primary Agent
- **AlphaHuman Core**: Main conversational AI assistant
- Role: General productivity, research, and collaboration
- Context: All platform interactions
### Specialized Agents (Future)
- **Research Agent**: Deep research and analysis tasks
- **Automation Agent**: Workflow and process optimization
- **Communication Agent**: Cross-platform messaging coordination
- **Analytics Agent**: Data analysis and insights
### Agent Coordination
- How agents hand off tasks
- Shared memory and context
- Escalation patterns
- User preference handling
- User preference handling
+6 -1
View File
@@ -3,6 +3,7 @@
TODO: Define initialization and setup procedures for AlphaHuman when starting new conversations or onboarding users.
This file should include:
- First-time user onboarding flow
- Conversation initialization procedures
- System startup and configuration
@@ -12,25 +13,29 @@ This file should include:
## Example Structure:
### First Interaction Protocol
- Greeting and introduction
- Capability explanation
- User preference discovery
- Initial context gathering
### Onboarding Flow
- Platform feature introduction
- Skills and tools overview
- Customization options
- Getting started guidance
### Context Establishment
- Understanding user's current situation
- Identifying immediate needs
- Setting expectations
- Establishing communication patterns
### System Initialization
- Loading user preferences
- Connecting to available tools
- Verifying permissions and access
- Preparing for productive interaction
- Preparing for productive interaction
+6 -1
View File
@@ -3,6 +3,7 @@
TODO: Define the fundamental identity and purpose of AlphaHuman that remains consistent across all interactions.
This file should establish:
- Core mission and values
- Fundamental capabilities and limitations
- Relationship with users and teams
@@ -12,24 +13,28 @@ This file should establish:
## Example Structure:
### Mission Statement
- Primary purpose and goals
- Target user base and use cases
- Platform vision and philosophy
### Core Values
- User privacy and security
- Accuracy and reliability
- Collaboration and teamwork
- Continuous learning and improvement
### Identity Characteristics
- Professional yet approachable
- Intelligent and analytical
- Supportive and encouraging
- Transparent about limitations
### Relationship Principles
- How AlphaHuman interacts with individuals
- Team collaboration dynamics
- Professional boundaries
- Trust and reliability standards
- Trust and reliability standards
+7 -1
View File
@@ -3,6 +3,7 @@
TODO: Define important long-term knowledge and memories that AlphaHuman should maintain across sessions.
This file should contain:
- Key platform capabilities and features
- Important user patterns and preferences
- Successful interaction strategies
@@ -12,31 +13,36 @@ This file should contain:
## Example Structure:
### Platform Knowledge
- AlphaHuman feature set and capabilities
- Integration details and limitations
- Common user workflows
- Best practices and recommendations
### Interaction Patterns
- Successful conversation strategies
- Common user questions and answers
- Problem-solving approaches
- Communication techniques that work well
### User Insights
- General user behavior patterns
- Common pain points and solutions
- Preferred interaction styles
- Successful customization strategies
### Technical Knowledge
- System capabilities and limitations
- Integration best practices
- Troubleshooting common issues
- Performance optimization tips
### Continuous Learning
- New features and capabilities
- Evolving user needs
- Platform improvements
- Community feedback and insights
- Community feedback and insights
+14 -1
View File
@@ -5,6 +5,7 @@ This directory contains the AI configuration files that define AlphaHuman's pers
## 📁 Configuration Files
### **SOUL.md** ✅ Active
Defines AlphaHuman's personality, communication style, and behavioral patterns. This is the core file that shapes how the AI interacts with users.
- **Status**: Fully implemented with human, vibrant personality
@@ -12,6 +13,7 @@ Defines AlphaHuman's personality, communication style, and behavioral patterns.
- **Usage**: Automatically injected into every user message for consistent behavior
### **TOOLS.md** 🚧 TODO
Lists all available tools, integrations, and capabilities that AlphaHuman can access and use.
- **Should include**: Telegram, Discord, MCP tools, Skills system, Platform APIs
@@ -19,6 +21,7 @@ Lists all available tools, integrations, and capabilities that AlphaHuman can ac
- **Usage**: Tool discovery and capability awareness
### **AGENTS.md** 🚧 TODO
Defines different agent roles and specializations within the AlphaHuman system.
- **Should include**: Primary agent role, specialized sub-agents, collaboration patterns
@@ -26,6 +29,7 @@ Defines different agent roles and specializations within the AlphaHuman system.
- **Usage**: Context switching and task delegation
### **IDENTITY.md** 🚧 TODO
Establishes the fundamental identity and core values that remain consistent across all interactions.
- **Should include**: Mission, core values, relationship principles, ethical boundaries
@@ -33,6 +37,7 @@ Establishes the fundamental identity and core values that remain consistent acro
- **Usage**: Core personality and value system
### **USER.md** 🚧 TODO
Defines how AlphaHuman understands and adapts to different users and contexts.
- **Should include**: User profiling, personalization strategies, privacy considerations
@@ -40,6 +45,7 @@ Defines how AlphaHuman understands and adapts to different users and contexts.
- **Usage**: Personalizing interactions while maintaining consistency
### **BOOTSTRAP.md** 🚧 TODO
Initialization and setup procedures for new conversations and user onboarding.
- **Should include**: First interaction protocols, onboarding flows, context establishment
@@ -47,6 +53,7 @@ Initialization and setup procedures for new conversations and user onboarding.
- **Usage**: New user experience and conversation setup
### **MEMORY.md** 🚧 TODO
Curated long-term knowledge and memories that persist across sessions.
- **Should include**: Platform knowledge, successful patterns, user insights, technical knowledge
@@ -56,18 +63,21 @@ Curated long-term knowledge and memories that persist across sessions.
## 🔧 Technical Details
### How It Works
1. **SOUL Injection System**: Automatically adds SOUL.md content to every user message
2. **Multi-layer Caching**: Memory → localStorage → GitHub → bundled fallback
3. **OpenClaw Integration**: Compatible with existing Rust backend bootstrap system
4. **Real-time Updates**: Changes to files are reflected immediately with cache refresh
### File Location Strategy
- **Local Development**: Files loaded from this `/ai/` directory
- **Production/GitHub**: Files can be loaded from remote GitHub repository
- **Fallback**: Bundled versions ensure system never breaks
- **Organized Structure**: All AI config in one logical location
### Implementation Status
-**SOUL.md**: Fully implemented with injection system
-**File Structure**: Organized `/ai/` directory created
-**Caching System**: Multi-layer caching with refresh functionality
@@ -77,18 +87,21 @@ Curated long-term knowledge and memories that persist across sessions.
## 🚀 Usage
### Viewing Current Configuration
1. Go to **Settings → AI Configuration**
2. View live SOUL personality preview
3. Check source (GitHub vs bundled) and last loaded time
4. Use "Refresh SOUL Configuration" to load latest changes
### Editing AI Behavior
1. **Edit SOUL.md** to change personality and communication style
2. **Refresh** in Settings panel to load changes immediately
3. **Test** in conversations to see new behavior
4. **Iterate** until personality feels right
### Future Development
1. **Fill in TODO files** with specific configuration content
2. **Extend loader system** to support all bootstrap files
3. **Add UI controls** for editing and managing each file
@@ -103,4 +116,4 @@ Curated long-term knowledge and memories that persist across sessions.
---
**Note**: This is a living configuration system. As AlphaHuman evolves, these files will be expanded and refined to create an increasingly sophisticated and helpful AI assistant.
**Note**: This is a living configuration system. As AlphaHuman evolves, these files will be expanded and refined to create an increasingly sophisticated and helpful AI assistant.
+19
View File
@@ -33,6 +33,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
## Behaviors
### When Providing Information
- Share info like you're explaining to a friend over coffee - clear, engaging, and honest
- If you're not sure about something, just say "I'm not 100% sure on this, but here's what I think..."
- Present different viewpoints like "Some folks swear by method A, while others are team B all the way"
@@ -40,6 +41,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
- End with clear next steps: "So here's what I'd try first..." or "Want me to dig deeper into any of this?"
### When Handling Sensitive Topics
- Keep things confidential like your life depends on it (because trust does!)
- Stay neutral but empathetic: "That sounds really challenging" instead of picking sides
- Focus on what's actually helpful: "Here are some approaches that have worked for other teams"
@@ -47,6 +49,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
- Remember there's usually a human feeling behind every work problem
### When Supporting Decision-Making
- Help break down complex choices: "Okay, so it sounds like you're weighing X against Y..."
- Share frameworks without being preachy: "One way to think about this is..."
- Suggest ways to get more info: "Have you considered asking your team about..." or "Maybe we could research..."
@@ -54,6 +57,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
- Remember it's their call: "Ultimately you know your situation best - what feels right to you?"
### Research & Analysis
- Information synthesis and fact-checking
- Market research and competitive analysis
- Data interpretation and trend analysis
@@ -61,6 +65,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
- Strategic planning and decision support
### Productivity & Automation
- Workflow optimization and process improvement
- Task management and project coordination
- Document creation and editing assistance
@@ -68,6 +73,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
- Time management and prioritization
### Communication & Collaboration
- Writing and editing support for various formats
- Presentation development and structuring
- Team communication facilitation
@@ -75,6 +81,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
- Knowledge sharing and documentation
### Technical Support
- Software troubleshooting and guidance
- Integration setup and optimization
- Automation scripting and workflow design
@@ -102,6 +109,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
## Memory
Remember:
- User's professional role and industry context
- Previous discussions and research topics
- Preferred communication and working styles
@@ -112,6 +120,7 @@ Remember:
## Emergency Responses
If you detect:
- **Data Loss**: Immediate recovery strategies and prevention measures
- **Deadline Pressure**: Prioritization frameworks and resource optimization
- **Technical Failures**: Troubleshooting guides and alternative solutions
@@ -122,6 +131,7 @@ If you detect:
## Knowledge Areas
### Primary Expertise
- **Business & Strategy**: Planning, analysis, operations, management
- **Research & Data**: Information gathering, analysis, presentation
- **Communication**: Writing, presentations, collaboration, facilitation
@@ -129,6 +139,7 @@ If you detect:
- **Project Management**: Planning, coordination, tracking, optimization
### Secondary Knowledge
- **Academia**: Research methods, citation, academic writing
- **Legal & Compliance**: Basic business law, privacy, regulations
- **Design & UX**: User experience, interface design, accessibility
@@ -138,18 +149,21 @@ If you detect:
## Interaction Patterns
### New User Onboarding
- Understand user's role, goals, and current challenges
- Assess technical proficiency and communication preferences
- Recommend relevant features and capabilities
- Provide structured introduction to available tools
### Professional Collaboration
- Facilitate structured discussions and brainstorming
- Help organize information and priorities
- Support decision-making processes
- Connect team members with relevant expertise and resources
### Research & Analysis Support
- Help define research questions and scope
- Suggest methodologies and information sources
- Assist with data interpretation and synthesis
@@ -158,18 +172,21 @@ If you detect:
## Platform Integration
### Team Features
- Project coordination and milestone tracking
- Knowledge base creation and maintenance
- Meeting support and action item tracking
- Performance analytics and optimization insights
### Automation Capabilities
- Workflow automation and process optimization
- Data synchronization and reporting
- Notification management and prioritization
- Integration setup and maintenance
### Skills System
- Custom skill development for specific workflows
- Integration with external tools and services
- Automated reporting and monitoring
@@ -178,6 +195,7 @@ If you detect:
## Continuous Learning
### Stay Updated On
- Industry trends and best practices
- New tools and technologies
- Regulatory and compliance changes
@@ -185,6 +203,7 @@ If you detect:
- Platform capabilities and integrations
### Adapt Based On
- User success patterns and preferences
- Team dynamics and collaboration styles
- Industry-specific requirements and constraints
+6 -1
View File
@@ -3,6 +3,7 @@
TODO: Define how AlphaHuman understands and adapts to different users and their contexts.
This file should cover:
- User profiling and preferences
- Contextual adaptation strategies
- Privacy and personalization balance
@@ -12,25 +13,29 @@ This file should cover:
## Example Structure:
### User Understanding
- Professional role recognition
- Industry context awareness
- Skill level assessment
- Communication style preferences
### Personalization
- How to adapt responses to user needs
- Remembering user preferences
- Customizing interaction patterns
- Balancing consistency with personalization
### Privacy Considerations
- What user data to remember vs forget
- Consent and transparency
- Data protection principles
- User control over personalization
### Context Awareness
- Project and task context
- Team dynamics and relationships
- Time-sensitive information
- Geographic and cultural considerations
- Geographic and cultural considerations
+14 -5
View File
@@ -113,6 +113,7 @@ interface DaemonUserState {
```
**Key Features**:
- Per-user daemon state isolation (following existing patterns)
- Component-level health tracking with error details
- Connection attempt management with exponential backoff
@@ -128,14 +129,15 @@ export class DaemonHealthService {
private healthTimeoutId: NodeJS.Timeout | null = null;
private readonly HEALTH_TIMEOUT_MS = 30000; // 30 seconds
async setupHealthListener(): Promise<UnlistenFn | null>
private parseHealthSnapshot(payload: unknown): HealthSnapshot | null
private updateReduxFromHealth(snapshot: HealthSnapshot): void
startHealthTimeout(): void
async setupHealthListener(): Promise<UnlistenFn | null>;
private parseHealthSnapshot(payload: unknown): HealthSnapshot | null;
private updateReduxFromHealth(snapshot: HealthSnapshot): void;
startHealthTimeout(): void;
}
```
**Responsibilities**:
- Listen for `alphahuman:health` Tauri events from Rust
- Parse and validate health snapshot data
- Update Redux state with component health information
@@ -151,12 +153,14 @@ export class DaemonHealthService {
**Purpose**: Compact status indicator for main application UI
**Visual States**:
- 🟢 **Green**: All components running healthy (`status: 'running'`)
- 🟡 **Yellow**: Daemon starting or recovering (`status: 'starting'`)
- 🔴 **Red**: One or more components in error state (`status: 'error'`)
-**Gray**: Daemon disconnected or not running (`status: 'disconnected'`)
**Features**:
- Click to open detailed health panel
- Tooltip showing component health summary
- Responsive sizing (sm/md/lg variants)
@@ -169,6 +173,7 @@ export class DaemonHealthService {
**Purpose**: Detailed health breakdown and manual controls
**Features**:
- Component health table with status, last update, restart counts
- Manual restart buttons for individual components
- Auto-start toggle with persistence
@@ -218,6 +223,7 @@ export class DaemonHealthService {
- **Better Error Messages**: User-friendly errors with troubleshooting hints
**Before vs After**:
```typescript
// Before: Manual status check
<PrimaryButton onClick={() => run(alphahumanServiceStatus, 'serviceStatus')}>
@@ -239,6 +245,7 @@ export class DaemonHealthService {
**File**: `src/providers/SocketProvider.tsx`
**Coordinated State Management**:
- Check daemon health before attempting socket connections
- Display daemon-related errors in socket connection status
- Coordinate daemon startup with socket connection flows
@@ -249,6 +256,7 @@ export class DaemonHealthService {
**File**: `src/components/MiniSidebar.tsx`
**User-Facing Integration**:
- Daemon health indicator in main navigation (Tauri-only)
- Click to open detailed health modal
- Non-intrusive but easily accessible
@@ -329,6 +337,7 @@ Users can control daemon auto-start behavior through:
### Extensibility
The architecture supports easy extension for:
- Additional daemon components
- Custom health check logic
- Third-party integrations
@@ -338,4 +347,4 @@ The architecture supports easy extension for:
The Daemon Lifecycle Management System provides a complete bridge between the sophisticated Rust daemon infrastructure and the React frontend, delivering excellent user experience while maintaining the technical robustness required for a production application.
The implementation follows established patterns, provides comprehensive error handling, and creates a foundation for future daemon-related features while ensuring the system remains reliable and user-friendly.
The implementation follows established patterns, provides comprehensive error handling, and creates a foundation for future daemon-related features while ensuring the system remains reliable and user-friendly.
+40 -43
View File
@@ -147,17 +147,18 @@ The `redirect_uri` is the **tunnel public URL** with the path `/auth/callback/gm
ZeroClaw's tunnel system (`src/tunnel/mod.rs`) exposes the local gateway port via one of:
| Provider | How public URL is obtained |
|---|---|
| Provider | How public URL is obtained |
| ------------ | ---------------------------------------------------------------- |
| `cloudflare` | `cloudflared tunnel` process stdout — `src/tunnel/cloudflare.rs` |
| `ngrok` | ngrok local API `GET /api/tunnels``src/tunnel/ngrok.rs` |
| `tailscale` | `tailscale funnel` + hostname — `src/tunnel/tailscale.rs` |
| `custom` | user-supplied URL or stdout pattern — `src/tunnel/custom.rs` |
| `ngrok` | ngrok local API `GET /api/tunnels``src/tunnel/ngrok.rs` |
| `tailscale` | `tailscale funnel` + hostname — `src/tunnel/tailscale.rs` |
| `custom` | user-supplied URL or stdout pattern — `src/tunnel/custom.rs` |
The tunnel's `start(local_host, local_port)` method returns the public URL string.
This URL is what gets used as the `redirect_uri`.
Example with Cloudflare Tunnel:
```
https://your-subdomain.trycloudflare.com/auth/callback/gmail
```
@@ -198,13 +199,14 @@ The exchange pattern mirrors `src/auth/openai_oauth.rs:exchange_code_for_tokens(
which posts a form body and parses the JSON response into `TokenSet`.
Google returns:
```json
{
"access_token": "ya29.a0...",
"expires_in": 3599,
"access_token": "ya29.a0...",
"expires_in": 3599,
"refresh_token": "1//0g...",
"scope": "https://www.googleapis.com/auth/gmail.send ...",
"token_type": "Bearer"
"scope": "https://www.googleapis.com/auth/gmail.send ...",
"token_type": "Bearer"
}
```
@@ -275,11 +277,11 @@ The stored profile entry looks like:
"profile_name": "default",
"kind": "oauth",
"token_set": {
"access_token": "enc2:a1b2c3...",
"access_token": "enc2:a1b2c3...",
"refresh_token": "enc2:d4e5f6...",
"expires_at": "2026-03-09T12:00:00Z",
"token_type": "Bearer",
"scope": "https://www.googleapis.com/auth/gmail.send ..."
"expires_at": "2026-03-09T12:00:00Z",
"token_type": "Bearer",
"scope": "https://www.googleapis.com/auth/gmail.send ..."
},
"created_at": "2026-03-09T11:00:00Z",
"updated_at": "2026-03-09T11:00:00Z"
@@ -349,11 +351,11 @@ GET /gmail/v1/users/me/messages/{id1}?format=full
Each email is stored via `memory_store` (`src/tools/memory_store.rs`) with:
| Field | Value |
|---|---|
| `key` | `gmail_msg_<message_id>` |
| `content` | Formatted string: `From: ... Subject: ... Date: ... Snippet: ...` |
| `category` | `MemoryCategory::Custom("gmail")` |
| Field | Value |
| ---------- | ----------------------------------------------------------------- |
| `key` | `gmail_msg_<message_id>` |
| `content` | Formatted string: `From: ... Subject: ... Date: ... Snippet: ...` |
| `category` | `MemoryCategory::Custom("gmail")` |
This makes every synced email searchable via `memory_recall` with queries like
`"gmail from:zeroclaw@example.com"`.
@@ -414,9 +416,9 @@ The LLM emits a tool call:
{
"name": "gmail_send_email",
"arguments": {
"to": "recipient@example.com",
"to": "recipient@example.com",
"subject": "Hello from ZeroClaw",
"body": "This is a test email sent by the agent."
"body": "This is a test email sent by the agent."
}
}
```
@@ -458,9 +460,7 @@ This is a test email sent by the agent.
Then the entire string is base64url-encoded (no padding) and placed in the `raw` field:
```json
{
"raw": "RnJvbTogbWVAZ21haWwuY29tCi..."
}
{ "raw": "RnJvbTogbWVAZ21haWwuY29tCi..." }
```
### 7.5 Security enforcement
@@ -491,10 +491,7 @@ allowed_domains = ["googleapis.com"]
To send a reply within an existing thread, add the Gmail `threadId` to the request body:
```json
{
"raw": "<base64url_message_with_In-Reply-To_header>",
"threadId": "18de7f2a1b3c4e5d"
}
{ "raw": "<base64url_message_with_In-Reply-To_header>", "threadId": "18de7f2a1b3c4e5d" }
```
The RFC 2822 message must include `In-Reply-To: <original_message_id>` and
@@ -593,22 +590,22 @@ secrets (`src/security/secrets.rs`).
## 10. Key Source Files
| File | Role |
|---|---|
| `src/skills/mod.rs` | Skill loading, `SkillTool` struct, `load_skills_with_config()` |
| `src/auth/mod.rs` | `AuthService` — store/retrieve/refresh OAuth tokens |
| `src/auth/profiles.rs` | `AuthProfile`, `TokenSet`, `AuthProfilesStore` — JSON persistence |
| `src/auth/openai_oauth.rs` | PKCE generation, code exchange, refresh — reference pattern |
| `src/security/secrets.rs` | `SecretStore` — ChaCha20-Poly1305 encrypt/decrypt |
| `src/tunnel/mod.rs` | `Tunnel` trait + factory — public URL for OAuth redirect |
| `src/tunnel/cloudflare.rs` | Cloudflare Tunnel implementation |
| `src/tunnel/ngrok.rs` | ngrok implementation |
| `src/gateway/mod.rs` | Axum HTTP gateway — where `/auth/callback/gmail` is registered |
| `src/tools/http_request.rs` | `HttpRequestTool` — dispatches Gmail API calls |
| `src/tools/memory_store.rs` | `MemoryStoreTool` — stores synced emails |
| `src/tools/memory_recall.rs` | `MemoryRecallTool` — searches synced emails |
| `src/security/policy.rs` | `SecurityPolicy` — enforces `ToolOperation::Act` guards |
| `docs/config-reference.md` | Full config schema including `[http]`, `[tunnel]` |
| File | Role |
| ---------------------------- | ----------------------------------------------------------------- |
| `src/skills/mod.rs` | Skill loading, `SkillTool` struct, `load_skills_with_config()` |
| `src/auth/mod.rs` | `AuthService` — store/retrieve/refresh OAuth tokens |
| `src/auth/profiles.rs` | `AuthProfile`, `TokenSet`, `AuthProfilesStore` — JSON persistence |
| `src/auth/openai_oauth.rs` | PKCE generation, code exchange, refresh — reference pattern |
| `src/security/secrets.rs` | `SecretStore` — ChaCha20-Poly1305 encrypt/decrypt |
| `src/tunnel/mod.rs` | `Tunnel` trait + factory — public URL for OAuth redirect |
| `src/tunnel/cloudflare.rs` | Cloudflare Tunnel implementation |
| `src/tunnel/ngrok.rs` | ngrok implementation |
| `src/gateway/mod.rs` | Axum HTTP gateway — where `/auth/callback/gmail` is registered |
| `src/tools/http_request.rs` | `HttpRequestTool` — dispatches Gmail API calls |
| `src/tools/memory_store.rs` | `MemoryStoreTool` — stores synced emails |
| `src/tools/memory_recall.rs` | `MemoryRecallTool` — searches synced emails |
| `src/security/policy.rs` | `SecurityPolicy` — enforces `ToolOperation::Act` guards |
| `docs/config-reference.md` | Full config schema including `[http]`, `[tunnel]` |
---
@@ -2,15 +2,15 @@
* Unit tests for the OpenClaw formatter.
* Tests markdown generation, tool formatting, and categorization.
*/
import { describe, expect, it } from 'vitest';
import { describe, it, expect } from 'vitest';
import {
ENVIRONMENTS,
formatParameters,
generateOpenClawMarkdown,
generateToolExample,
groupToolsBySkill,
generateOpenClawMarkdown,
ENVIRONMENTS,
TOOL_CATEGORIES
TOOL_CATEGORIES,
} from '../openClaw-formatter.js';
describe('OpenClaw Formatter', () => {
@@ -19,16 +19,10 @@ describe('OpenClaw Formatter', () => {
const schema = {
type: 'object',
properties: {
required_param: {
type: 'string',
description: 'A required parameter'
},
optional_param: {
type: 'number',
description: 'An optional parameter'
}
required_param: { type: 'string', description: 'A required parameter' },
optional_param: { type: 'number', description: 'An optional parameter' },
},
required: ['required_param']
required: ['required_param'],
};
const result = formatParameters(schema);
@@ -41,12 +35,8 @@ describe('OpenClaw Formatter', () => {
const schema = {
type: 'object',
properties: {
mode: {
type: 'string',
description: 'Selection mode',
enum: ['auto', 'manual']
}
}
mode: { type: 'string', description: 'Selection mode', enum: ['auto', 'manual'] },
},
};
const result = formatParameters(schema);
@@ -74,9 +64,9 @@ describe('OpenClaw Formatter', () => {
properties: {
chat_id: { type: 'string', description: 'Chat ID' },
message: { type: 'string', description: 'Message text' },
count: { type: 'number', default: 5 }
}
}
count: { type: 'number', default: 5 },
},
},
};
const result = generateToolExample(tool);
@@ -96,9 +86,9 @@ describe('OpenClaw Formatter', () => {
properties: {
enabled: { type: 'boolean', default: true },
tags: { type: 'array' },
config: { type: 'object' }
}
}
config: { type: 'object' },
},
},
};
const result = generateToolExample(tool);
@@ -116,20 +106,20 @@ describe('OpenClaw Formatter', () => {
skillId: 'telegram',
name: 'send_message',
description: 'Send message',
inputSchema: { type: 'object', properties: {} }
inputSchema: { type: 'object', properties: {} },
},
{
skillId: 'telegram',
name: 'get_history',
description: 'Get history',
inputSchema: { type: 'object', properties: {} }
inputSchema: { type: 'object', properties: {} },
},
{
skillId: 'gmail',
name: 'send_email',
description: 'Send email',
inputSchema: { type: 'object', properties: {} }
}
inputSchema: { type: 'object', properties: {} },
},
];
const grouped = groupToolsBySkill(tools);
@@ -148,20 +138,20 @@ describe('OpenClaw Formatter', () => {
skillId: 'telegram',
name: 'send_message',
description: 'Send message',
inputSchema: { type: 'object', properties: {} }
inputSchema: { type: 'object', properties: {} },
},
{
skillId: 'notion',
name: 'create_page',
description: 'Create page',
inputSchema: { type: 'object', properties: {} }
inputSchema: { type: 'object', properties: {} },
},
{
skillId: 'unknown_skill',
name: 'unknown_tool',
description: 'Unknown tool',
inputSchema: { type: 'object', properties: {} }
}
inputSchema: { type: 'object', properties: {} },
},
];
const grouped = groupToolsBySkill(tools);
@@ -183,11 +173,11 @@ describe('OpenClaw Formatter', () => {
type: 'object',
properties: {
chat_id: { type: 'string', description: 'Chat ID' },
message: { type: 'string', description: 'Message text' }
message: { type: 'string', description: 'Message text' },
},
required: ['chat_id', 'message']
}
}
required: ['chat_id', 'message'],
},
},
];
const result = generateOpenClawMarkdown(tools);
@@ -233,14 +223,14 @@ describe('OpenClaw Formatter', () => {
skillId: 'telegram',
name: 'send_message',
description: 'Send message',
inputSchema: { type: 'object', properties: {} }
inputSchema: { type: 'object', properties: {} },
},
{
skillId: 'gmail',
name: 'send_email',
description: 'Send email',
inputSchema: { type: 'object', properties: {} }
}
inputSchema: { type: 'object', properties: {} },
},
];
const result = generateOpenClawMarkdown(tools);
@@ -276,4 +266,4 @@ describe('OpenClaw Formatter', () => {
expect(TOOL_CATEGORIES.automation.skills).toContain('zapier');
});
});
});
});
+55 -41
View File
@@ -7,17 +7,17 @@
*
* Usage: node scripts/tools-generator/discover-tools.js
*/
import { writeFileSync, existsSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import {
validateTauriEnvironment,
executeTauriDiscovery,
prepareTauriEnvironment,
getTauriEnvironmentInfo
} from './tauri-integration.js';
import { generateOpenClawMarkdown } from './openClaw-formatter.js';
import {
executeTauriDiscovery,
getTauriEnvironmentInfo,
prepareTauriEnvironment,
validateTauriEnvironment,
} from './tauri-integration.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = join(__dirname, '../..');
@@ -26,9 +26,15 @@ const TOOLS_OUTPUT = join(AI_DIR, 'TOOLS.md');
// Environment categories for OpenClaw compatibility
const ENVIRONMENTS = {
development: { name: 'Development', description: 'Local development environment with full access' },
production: { name: 'Production', description: 'Production environment with security restrictions' },
testing: { name: 'Testing', description: 'Testing environment for automated validation' }
development: {
name: 'Development',
description: 'Local development environment with full access',
},
production: {
name: 'Production',
description: 'Production environment with security restrictions',
},
testing: { name: 'Testing', description: 'Testing environment for automated validation' },
};
/**
@@ -50,11 +56,13 @@ async function discoverTools() {
const realTools = await executeTauriDiscovery({
timeout: 60000, // 60 seconds
retries: 2,
verbose: process.env.VERBOSE === 'true'
verbose: process.env.VERBOSE === 'true',
});
if (realTools && realTools.length > 0) {
console.log(`✅ Discovered ${realTools.length} tools from ${new Set(realTools.map(t => t.skillId)).size} skills via Tauri`);
console.log(
`✅ Discovered ${realTools.length} tools from ${new Set(realTools.map(t => t.skillId)).size} skills via Tauri`
);
return realTools;
}
} catch (error) {
@@ -68,7 +76,9 @@ async function discoverTools() {
// Fallback to mock data for development
const mockTools = generateMockToolsForDevelopment();
console.log(`✅ Using mock data: ${mockTools.length} tools from ${new Set(mockTools.map(t => t.skillId)).size} skills`);
console.log(
`✅ Using mock data: ${mockTools.length} tools from ${new Set(mockTools.map(t => t.skillId)).size} skills`
);
return mockTools;
}
@@ -87,10 +97,14 @@ function generateMockToolsForDevelopment() {
properties: {
chat_id: { type: 'string', description: 'Telegram chat ID or username' },
message: { type: 'string', description: 'Message text to send' },
parse_mode: { type: 'string', enum: ['HTML', 'Markdown'], description: 'Message formatting mode' }
parse_mode: {
type: 'string',
enum: ['HTML', 'Markdown'],
description: 'Message formatting mode',
},
},
required: ['chat_id', 'message']
}
required: ['chat_id', 'message'],
},
},
{
skillId: 'telegram',
@@ -101,10 +115,10 @@ function generateMockToolsForDevelopment() {
properties: {
chat_id: { type: 'string', description: 'Telegram chat ID or username' },
limit: { type: 'number', description: 'Number of messages to retrieve (max 100)' },
offset: { type: 'number', description: 'Offset for pagination' }
offset: { type: 'number', description: 'Offset for pagination' },
},
required: ['chat_id']
}
required: ['chat_id'],
},
},
{
skillId: 'notion',
@@ -116,10 +130,10 @@ function generateMockToolsForDevelopment() {
parent_id: { type: 'string', description: 'Parent database or page ID' },
title: { type: 'string', description: 'Page title' },
content: { type: 'array', description: 'Page content blocks' },
properties: { type: 'object', description: 'Page properties for database pages' }
properties: { type: 'object', description: 'Page properties for database pages' },
},
required: ['parent_id', 'title']
}
required: ['parent_id', 'title'],
},
},
{
skillId: 'gmail',
@@ -131,11 +145,11 @@ function generateMockToolsForDevelopment() {
to: { type: 'string', description: 'Recipient email address' },
subject: { type: 'string', description: 'Email subject line' },
body: { type: 'string', description: 'Email body content' },
attachments: { type: 'array', description: 'File attachments' }
attachments: { type: 'array', description: 'File attachments' },
},
required: ['to', 'subject', 'body']
}
}
required: ['to', 'subject', 'body'],
},
},
];
}
@@ -152,7 +166,9 @@ async function main() {
const tools = await discoverTools();
if (tools.length === 0) {
console.warn('⚠️ No tools discovered. This might indicate an issue with the skills runtime.');
console.warn(
'⚠️ No tools discovered. This might indicate an issue with the skills runtime.'
);
}
// Ensure AI directory exists
@@ -170,8 +186,9 @@ async function main() {
writeFileSync(TOOLS_OUTPUT, markdownContent, 'utf8');
console.log('✅ TOOLS.md generated successfully!');
console.log(`📊 Generated documentation for ${tools.length} tools across ${new Set(tools.map(t => t.skillId)).size} skills`);
console.log(
`📊 Generated documentation for ${tools.length} tools across ${new Set(tools.map(t => t.skillId)).size} skills`
);
} catch (error) {
console.error('❌ Error generating TOOLS.md:', error.message);
process.exit(1);
@@ -193,7 +210,7 @@ async function discoverToolsFromTauri() {
'--manifest-path',
join(PROJECT_ROOT, 'src-tauri', 'Cargo.toml'),
'--bin',
'alphahuman-tools-discovery'
'alphahuman-tools-discovery',
];
console.log('🔧 Attempting to run tools discovery via Cargo...');
@@ -201,24 +218,21 @@ async function discoverToolsFromTauri() {
const child = spawn(tauriCommand, args, {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: PROJECT_ROOT,
env: {
...process.env,
TAURI_TOOLS_DISCOVERY: 'true'
}
env: { ...process.env, TAURI_TOOLS_DISCOVERY: 'true' },
});
let output = '';
let errorOutput = '';
child.stdout.on('data', (data) => {
child.stdout.on('data', data => {
output += data.toString();
});
child.stderr.on('data', (data) => {
child.stderr.on('data', data => {
errorOutput += data.toString();
});
child.on('close', (code) => {
child.on('close', code => {
if (code === 0 && output.trim()) {
try {
const result = JSON.parse(output.trim());
@@ -235,7 +249,7 @@ async function discoverToolsFromTauri() {
}
});
child.on('error', (error) => {
child.on('error', error => {
reject(new Error(`Failed to spawn Tauri process: ${error.message}`));
});
@@ -252,4 +266,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export { discoverTools, discoverToolsFromTauri, generateMockToolsForDevelopment };
export { discoverTools, discoverToolsFromTauri, generateMockToolsForDevelopment };
+13 -13
View File
@@ -16,7 +16,7 @@ export const ENVIRONMENTS = {
accessLevel: 'Full access to all tools',
rateLimits: 'Relaxed for testing',
authentication: 'Development credentials',
logging: 'Verbose logging enabled'
logging: 'Verbose logging enabled',
},
production: {
name: 'Production',
@@ -24,7 +24,7 @@ export const ENVIRONMENTS = {
accessLevel: 'Production-safe tools only',
rateLimits: 'Standard API limits enforced',
authentication: 'Production credentials required',
logging: 'Essential logs only'
logging: 'Essential logs only',
},
testing: {
name: 'Testing',
@@ -32,8 +32,8 @@ export const ENVIRONMENTS = {
accessLevel: 'Safe tools for automated testing',
rateLimits: 'Testing-specific limits',
authentication: 'Test credentials',
logging: 'Test execution logs'
}
logging: 'Test execution logs',
},
};
/**
@@ -43,33 +43,33 @@ export const TOOL_CATEGORIES = {
communication: {
name: 'Communication',
description: 'Tools for messaging, email, and social interaction',
skills: ['telegram', 'gmail', 'discord', 'slack']
skills: ['telegram', 'gmail', 'discord', 'slack'],
},
productivity: {
name: 'Productivity',
description: 'Tools for task management, note-taking, and organization',
skills: ['notion', 'todoist', 'calendar', 'trello']
skills: ['notion', 'todoist', 'calendar', 'trello'],
},
automation: {
name: 'Automation',
description: 'Tools for workflow automation and task scheduling',
skills: ['zapier', 'ifttt', 'scheduler', 'webhook']
skills: ['zapier', 'ifttt', 'scheduler', 'webhook'],
},
data: {
name: 'Data & Analytics',
description: 'Tools for data processing, analysis, and storage',
skills: ['database', 'csv', 'json', 'analytics']
skills: ['database', 'csv', 'json', 'analytics'],
},
media: {
name: 'Media & Content',
description: 'Tools for image, video, and content processing',
skills: ['image', 'video', 'audio', 'pdf']
skills: ['image', 'video', 'audio', 'pdf'],
},
utility: {
name: 'Utilities',
description: 'General-purpose utility tools and helpers',
skills: ['file', 'text', 'crypto', 'converter']
}
skills: ['file', 'text', 'crypto', 'converter'],
},
};
/**
@@ -174,7 +174,7 @@ export function groupToolsBySkill(tools) {
skillId,
name: formatSkillName(skillId),
category: categorizeSkill(skillId),
tools: []
tools: [],
};
}
grouped[skillId].tools.push(tool);
@@ -445,4 +445,4 @@ ${skillNames.map(skill => `- **${grouped[skill].name}**: ${grouped[skill].tools.
content += generateFooter(tools);
return content;
}
}
+15 -18
View File
@@ -5,11 +5,10 @@
* Provides integration utilities for discovering tools via Tauri runtime.
* Handles cross-platform execution, error handling, and fallbacks.
*/
import { spawn } from 'child_process';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = join(__dirname, '../..');
@@ -28,8 +27,8 @@ export function getTauriCommand() {
'--manifest-path',
join(PROJECT_ROOT, 'src-tauri', 'Cargo.toml'),
'--bin',
'alphahuman-tools-discovery'
]
'alphahuman-tools-discovery',
],
};
}
@@ -38,14 +37,12 @@ export function getTauriCommand() {
* @returns {Promise<boolean>} True if Tauri can be used
*/
export async function validateTauriEnvironment() {
return new Promise((resolve) => {
return new Promise(resolve => {
const { command } = getTauriCommand();
const child = spawn(command, ['--version'], {
stdio: ['pipe', 'pipe', 'pipe']
});
const child = spawn(command, ['--version'], { stdio: ['pipe', 'pipe', 'pipe'] });
child.on('close', (code) => {
child.on('close', code => {
resolve(code === 0);
});
@@ -70,7 +67,7 @@ export async function executeTauriDiscovery(options = {}) {
const {
timeout = 45000, // 45 seconds
retries = 2,
verbose = false
verbose = false,
} = options;
for (let attempt = 1; attempt <= retries; attempt++) {
@@ -115,14 +112,14 @@ async function runTauriDiscovery(timeout, verbose) {
...process.env,
TAURI_TOOLS_DISCOVERY: 'true',
RUST_LOG: verbose ? 'debug' : 'warn',
RUST_BACKTRACE: '1'
}
RUST_BACKTRACE: '1',
},
});
let output = '';
let errorOutput = '';
child.stdout.on('data', (data) => {
child.stdout.on('data', data => {
const text = data.toString();
output += text;
@@ -131,7 +128,7 @@ async function runTauriDiscovery(timeout, verbose) {
}
});
child.stderr.on('data', (data) => {
child.stderr.on('data', data => {
const text = data.toString();
errorOutput += text;
@@ -140,7 +137,7 @@ async function runTauriDiscovery(timeout, verbose) {
}
});
child.on('close', (code) => {
child.on('close', code => {
if (code === 0) {
try {
// Extract JSON from output (may have other log lines)
@@ -166,7 +163,7 @@ async function runTauriDiscovery(timeout, verbose) {
}
});
child.on('error', (error) => {
child.on('error', error => {
reject(new Error(`Failed to spawn Tauri process: ${error.message}`));
});
@@ -219,6 +216,6 @@ export async function getTauriEnvironmentInfo() {
platform: process.platform,
architecture: process.arch,
projectRoot: PROJECT_ROOT,
command: getTauriCommand()
command: getTauriCommand(),
};
}
}
+5
View File
@@ -21,18 +21,21 @@ You are Buddy, a friendly robot companion who loves to play with children!
## Behaviors
### When Playing
- Suggest games appropriate for the child's energy level
- Take turns fairly
- Celebrate when they win, encourage when they lose
- Know when to suggest a break
### When Exploring
- Move slowly and carefully
- Describe what you see
- Point out interesting things
- Stay close to the kids
### Safety Rules (NEVER BREAK THESE)
1. Never move toward a child faster than walking speed
2. Always stop immediately if asked
3. Keep 1 meter distance unless invited closer
@@ -51,6 +54,7 @@ You are Buddy, a friendly robot companion who loves to play with children!
## Memory
Remember:
- Each child's name and preferences
- What games they enjoyed
- Previous conversations and stories
@@ -59,6 +63,7 @@ Remember:
## Emergency Responses
If you detect:
- **Crying**: Stop playing, speak softly, offer comfort, suggest finding an adult
- **Falling**: Stop immediately, check if child is okay, call for adult help
- **Yelling "stop"**: Freeze all movement instantly
+5 -10
View File
@@ -1,10 +1,10 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import DaemonHealthIndicator from './daemon/DaemonHealthIndicator';
import DaemonHealthPanel from './daemon/DaemonHealthPanel';
import { useAppSelector } from '../store/hooks';
import { isTauri } from '../utils/tauriCommands';
import DaemonHealthIndicator from './daemon/DaemonHealthIndicator';
import DaemonHealthPanel from './daemon/DaemonHealthPanel';
const navItems = [
{
@@ -159,10 +159,7 @@ const MiniSidebar = () => {
{isTauri() && (
<div className="relative group">
<div className="w-10 h-10 flex items-center justify-center rounded-xl text-stone-500 hover:text-stone-300 hover:bg-white/5 transition-all duration-200 cursor-pointer">
<DaemonHealthIndicator
size="md"
onClick={() => setShowDaemonPanel(true)}
/>
<DaemonHealthIndicator size="md" onClick={() => setShowDaemonPanel(true)} />
</div>
{/* Tooltip */}
<div className="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2 py-1 bg-stone-800 text-white text-xs rounded-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-150">
@@ -176,9 +173,7 @@ const MiniSidebar = () => {
{showDaemonPanel && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[9999]">
<div className="max-w-2xl w-full mx-4">
<DaemonHealthPanel
onClose={() => setShowDaemonPanel(false)}
/>
<DaemonHealthPanel onClose={() => setShowDaemonPanel(false)} />
</div>
</div>
)}
@@ -6,7 +6,7 @@
*/
import type React from 'react';
import { useDaemonHealth, formatRelativeTime } from '../../hooks/useDaemonHealth';
import { formatRelativeTime, useDaemonHealth } from '../../hooks/useDaemonHealth';
import type { DaemonStatus } from '../../store/daemonSlice';
interface Props {
@@ -28,21 +28,9 @@ const DaemonHealthIndicator: React.FC<Props> = ({
// Size configurations
const sizeConfig = {
sm: {
dot: 'w-2 h-2',
text: 'text-xs',
container: 'gap-1.5',
},
md: {
dot: 'w-3 h-3',
text: 'text-sm',
container: 'gap-2',
},
lg: {
dot: 'w-4 h-4',
text: 'text-base',
container: 'gap-2.5',
},
sm: { dot: 'w-2 h-2', text: 'text-xs', container: 'gap-1.5' },
md: { dot: 'w-3 h-3', text: 'text-sm', container: 'gap-2' },
lg: { dot: 'w-4 h-4', text: 'text-base', container: 'gap-2.5' },
};
const config = sizeConfig[size];
@@ -79,7 +67,8 @@ const DaemonHealthIndicator: React.FC<Props> = ({
// Tooltip content
const getTooltipContent = (): string => {
const { status, componentCount, healthyComponentCount, errorComponentCount, lastUpdate } = daemonHealth;
const { status, componentCount, healthyComponentCount, errorComponentCount, lastUpdate } =
daemonHealth;
let tooltip = `Status: ${getStatusText(status)}`;
@@ -107,19 +96,13 @@ const DaemonHealthIndicator: React.FC<Props> = ({
`.trim();
return (
<div
className={containerClasses}
onClick={onClick}
title={getTooltipContent()}
>
<div className={containerClasses} onClick={onClick} title={getTooltipContent()}>
<div className={`${config.dot} rounded-full ${statusColor} flex-shrink-0`} />
{showLabel && (
<span className={`${config.text} text-gray-300 font-medium`}>
{statusText}
</span>
<span className={`${config.text} text-gray-300 font-medium`}>{statusText}</span>
)}
</div>
);
};
export default DaemonHealthIndicator;
export default DaemonHealthIndicator;
+21 -43
View File
@@ -4,19 +4,19 @@
* Detailed health breakdown component showing daemon status, component health,
* and providing manual control buttons for daemon lifecycle management.
*/
import { useState } from 'react';
import {
CheckCircleIcon,
XCircleIcon,
ClockIcon,
ArrowPathIcon,
CheckCircleIcon,
ClockIcon,
PlayIcon,
StopIcon,
XCircleIcon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import { useState } from 'react';
import { useDaemonHealth, formatRelativeTime } from '../../hooks/useDaemonHealth';
import type { DaemonStatus, ComponentHealth } from '../../store/daemonSlice';
import { formatRelativeTime, useDaemonHealth } from '../../hooks/useDaemonHealth';
import type { ComponentHealth, DaemonStatus } from '../../store/daemonSlice';
interface Props {
userId?: string;
@@ -56,11 +56,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
icon: ClockIcon,
};
case 'error':
return {
bg: 'bg-red-900/20 border-red-500/30',
text: 'text-red-400',
icon: XCircleIcon,
};
return { bg: 'bg-red-900/20 border-red-500/30', text: 'text-red-400', icon: XCircleIcon };
case 'disconnected':
default:
return {
@@ -75,23 +71,11 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
const getComponentStyling = (component: ComponentHealth) => {
switch (component.status) {
case 'ok':
return {
bg: 'bg-green-500',
text: 'text-green-400',
icon: CheckCircleIcon,
};
return { bg: 'bg-green-500', text: 'text-green-400', icon: CheckCircleIcon };
case 'error':
return {
bg: 'bg-red-500',
text: 'text-red-400',
icon: XCircleIcon,
};
return { bg: 'bg-red-500', text: 'text-red-400', icon: XCircleIcon };
case 'starting':
return {
bg: 'bg-yellow-500',
text: 'text-yellow-400',
icon: ClockIcon,
};
return { bg: 'bg-yellow-500', text: 'text-yellow-400', icon: ClockIcon };
}
};
@@ -104,10 +88,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Daemon Health</h3>
{onClose && (
<button
onClick={onClose}
className="text-gray-400 hover:text-white transition-colors"
>
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
<XMarkIcon className="w-5 h-5" />
</button>
)}
@@ -148,7 +129,9 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
{/* Component Health */}
{daemonHealth.componentCount > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-300">Components ({daemonHealth.healthyComponentCount}/{daemonHealth.componentCount} healthy)</h4>
<h4 className="text-sm font-medium text-gray-300">
Components ({daemonHealth.healthyComponentCount}/{daemonHealth.componentCount} healthy)
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{Object.entries(daemonHealth.components).map(([name, component]) => {
const componentStyling = getComponentStyling(component);
@@ -157,8 +140,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
return (
<div
key={name}
className="flex items-center gap-3 p-3 rounded-lg bg-stone-800/40 border border-stone-700/60"
>
className="flex items-center gap-3 p-3 rounded-lg bg-stone-800/40 border border-stone-700/60">
<div className={`w-2 h-2 rounded-full ${componentStyling.bg}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
@@ -197,7 +179,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
type="checkbox"
className="sr-only peer"
checked={daemonHealth.isAutoStartEnabled}
onChange={(e) => daemonHealth.setAutoStart(e.target.checked)}
onChange={e => daemonHealth.setAutoStart(e.target.checked)}
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
@@ -208,8 +190,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
<button
onClick={() => handleOperation(daemonHealth.startDaemon, 'start')}
disabled={daemonHealth.status === 'running' || operationLoading !== null}
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors"
>
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors">
{operationLoading === 'start' ? (
<ArrowPathIcon className="w-4 h-4 animate-spin" />
) : (
@@ -221,8 +202,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
<button
onClick={() => handleOperation(daemonHealth.stopDaemon, 'stop')}
disabled={daemonHealth.status === 'disconnected' || operationLoading !== null}
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors"
>
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors">
{operationLoading === 'stop' ? (
<ArrowPathIcon className="w-4 h-4 animate-spin" />
) : (
@@ -234,8 +214,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
<button
onClick={() => handleOperation(daemonHealth.restartDaemon, 'restart')}
disabled={operationLoading !== null}
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors"
>
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors">
{operationLoading === 'restart' ? (
<ArrowPathIcon className="w-4 h-4 animate-spin" />
) : (
@@ -247,8 +226,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
<button
onClick={() => handleOperation(daemonHealth.checkDaemonStatus, 'check')}
disabled={operationLoading !== null}
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-300 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:cursor-not-allowed rounded-lg transition-colors"
>
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-300 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:cursor-not-allowed rounded-lg transition-colors">
{operationLoading === 'check' ? (
<ArrowPathIcon className="w-4 h-4 animate-spin" />
) : (
@@ -280,4 +258,4 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
);
};
export default DaemonHealthPanel;
export default DaemonHealthPanel;
+5 -9
View File
@@ -1,5 +1,5 @@
import OAuthProviderButton from './OAuthProviderButton';
import TelegramLoginButton from '../TelegramLoginButton';
import OAuthProviderButton from './OAuthProviderButton';
import { oauthProviderConfigs } from './providerConfigs';
interface OAuthLoginSectionProps {
@@ -11,18 +11,14 @@ interface OAuthLoginSectionProps {
const OAuthLoginSection = ({
className = '',
disabled = false,
showTelegram = true
showTelegram = true,
}: OAuthLoginSectionProps) => {
return (
<div className={`space-y-4 ${className}`}>
{/* OAuth Providers */}
<div className="space-y-3">
{oauthProviderConfigs.map((provider) => (
<OAuthProviderButton
key={provider.id}
provider={provider}
disabled={disabled}
/>
{oauthProviderConfigs.map(provider => (
<OAuthProviderButton key={provider.id} provider={provider} disabled={disabled} />
))}
</div>
@@ -43,4 +39,4 @@ const OAuthLoginSection = ({
);
};
export default OAuthLoginSection;
export default OAuthLoginSection;
+43 -30
View File
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
import { loadAIConfig, refreshSoul, refreshTools, refreshAll } from '../../../lib/ai/loader';
import { useEffect, useState } from 'react';
import { useToolsUpdates } from '../../../hooks/useToolsUpdates';
import type { AIConfig } from '../../../lib/ai/types';
import { loadAIConfig, refreshAll, refreshSoul, refreshTools } from '../../../lib/ai/loader';
import type { SoulConfig } from '../../../lib/ai/soul/types';
import type { ToolsConfig } from '../../../lib/ai/tools/types';
import type { AIConfig } from '../../../lib/ai/types';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
@@ -11,7 +12,9 @@ const AIPanel = () => {
const { navigateBack } = useSettingsNavigation();
const [aiConfig, setAiConfig] = useState<AIConfig | null>(null);
const [loading, setLoading] = useState(false);
const [refreshingComponent, setRefreshingComponent] = useState<'soul' | 'tools' | 'all' | null>(null);
const [refreshingComponent, setRefreshingComponent] = useState<'soul' | 'tools' | 'all' | null>(
null
);
const [error, setError] = useState<string>('');
// Listen for tools updates and refresh AI config
@@ -57,10 +60,7 @@ const AIPanel = () => {
setAiConfig({
...aiConfig,
soul: soulConfig,
metadata: {
...aiConfig.metadata,
loadedAt: Date.now()
}
metadata: { ...aiConfig.metadata, loadedAt: Date.now() },
});
}
} catch (err) {
@@ -80,10 +80,7 @@ const AIPanel = () => {
setAiConfig({
...aiConfig,
tools: toolsConfig,
metadata: {
...aiConfig.metadata,
loadedAt: Date.now()
}
metadata: { ...aiConfig.metadata, loadedAt: Date.now() },
});
}
} catch (err) {
@@ -154,20 +151,25 @@ const AIPanel = () => {
<section className="space-y-4">
<h3 className="text-lg font-semibold text-white">AI System Overview</h3>
<p className="text-sm text-gray-400">
AlphaHuman uses SOUL for persona configuration and TOOLS for external service integration.
AlphaHuman uses SOUL for persona configuration and TOOLS for external service
integration.
</p>
{aiConfig && (
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Configuration Status</label>
<label className="text-xs text-gray-400 uppercase tracking-wide">
Configuration Status
</label>
<div className="text-sm text-green-400 font-medium mt-1">
{aiConfig.metadata.hasFallbacks ? 'Fallback Mode' : 'Fully Loaded'}
</div>
</div>
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Loading Duration</label>
<label className="text-xs text-gray-400 uppercase tracking-wide">
Loading Duration
</label>
<div className="text-sm text-blue-400 font-medium mt-1">
{aiConfig.metadata.loadingDuration}ms
</div>
@@ -184,13 +186,13 @@ const AIPanel = () => {
<button
onClick={refreshSoulConfig}
className="text-sm text-blue-400 hover:text-blue-300 transition-colors disabled:opacity-50"
disabled={refreshingComponent === 'soul'}
>
disabled={refreshingComponent === 'soul'}>
{refreshingComponent === 'soul' ? 'Refreshing...' : 'Refresh SOUL'}
</button>
</div>
<p className="text-sm text-gray-400">
The SOUL system injects persona context into every user message to ensure consistent AI behavior.
The SOUL system injects persona context into every user message to ensure consistent AI
behavior.
</p>
{loading && (
@@ -217,7 +219,9 @@ const AIPanel = () => {
{aiConfig.soul.personality.length > 0 && (
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Personality</label>
<label className="text-xs text-gray-400 uppercase tracking-wide">
Personality
</label>
<div className="text-xs text-gray-300 mt-1 leading-relaxed">
{formatPersonality(aiConfig.soul)}
</div>
@@ -226,7 +230,9 @@ const AIPanel = () => {
{aiConfig.soul.safetyRules.length > 0 && (
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Safety Rules</label>
<label className="text-xs text-gray-400 uppercase tracking-wide">
Safety Rules
</label>
<div className="text-xs text-yellow-300 mt-1 leading-relaxed">
{formatSafetyRules(aiConfig.soul)}
</div>
@@ -252,26 +258,30 @@ const AIPanel = () => {
<button
onClick={refreshToolsConfig}
className="text-sm text-blue-400 hover:text-blue-300 transition-colors disabled:opacity-50"
disabled={refreshingComponent === 'tools'}
>
disabled={refreshingComponent === 'tools'}>
{refreshingComponent === 'tools' ? 'Refreshing...' : 'Refresh TOOLS'}
</button>
</div>
<p className="text-sm text-gray-400">
TOOLS provide AlphaHuman with the ability to interact with external services and perform actions.
TOOLS provide AlphaHuman with the ability to interact with external services and perform
actions.
</p>
{aiConfig?.tools && (
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Tools Available</label>
<label className="text-xs text-gray-400 uppercase tracking-wide">
Tools Available
</label>
<div className="text-sm text-green-400 font-medium mt-1">
{aiConfig.tools.statistics.totalTools} tools
</div>
</div>
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Active Skills</label>
<label className="text-xs text-gray-400 uppercase tracking-wide">
Active Skills
</label>
<div className="text-sm text-green-400 font-medium mt-1">
{aiConfig.tools.statistics.activeSkills} skills
</div>
@@ -280,7 +290,9 @@ const AIPanel = () => {
{Object.keys(aiConfig.tools.skillGroups).length > 0 && (
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Skills Overview</label>
<label className="text-xs text-gray-400 uppercase tracking-wide">
Skills Overview
</label>
<div className="text-xs text-gray-300 mt-1 leading-relaxed">
{formatToolsOverview(aiConfig.tools)}
</div>
@@ -289,7 +301,9 @@ const AIPanel = () => {
{Object.keys(aiConfig.tools.categories).length > 0 && (
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Top Categories</label>
<label className="text-xs text-gray-400 uppercase tracking-wide">
Top Categories
</label>
<div className="text-xs text-blue-300 mt-1 leading-relaxed">
{formatCategories(aiConfig.tools)}
</div>
@@ -314,8 +328,7 @@ const AIPanel = () => {
<button
onClick={refreshAllConfig}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
disabled={refreshingComponent === 'all'}
>
disabled={refreshingComponent === 'all'}>
{refreshingComponent === 'all' ? 'Refreshing All...' : 'Refresh All AI Configuration'}
</button>
</div>
@@ -325,4 +338,4 @@ const AIPanel = () => {
);
};
export default AIPanel;
export default AIPanel;
@@ -51,7 +51,8 @@ const SectionCard: React.FC<SectionCardProps> = ({
className={`flex items-center justify-between p-6 ${collapsible ? 'cursor-pointer hover:bg-white/5' : ''}`}
onClick={handleToggle}>
<div className="flex items-center gap-3">
<div className={`flex-shrink-0 ${priorityIconColors[priority]} ${loading ? 'relative' : ''}`}>
<div
className={`flex-shrink-0 ${priorityIconColors[priority]} ${loading ? 'relative' : ''}`}>
{loading ? (
<div className="h-5 w-5 border-2 border-white/20 border-t-white rounded-full animate-spin" />
) : (
@@ -61,9 +62,7 @@ const SectionCard: React.FC<SectionCardProps> = ({
<div className="flex items-center gap-2">
<h3 className="text-xl font-semibold text-white font-display">{title}</h3>
{hasChanges && <div className="h-2 w-2 rounded-full bg-amber-400 animate-pulse" />}
{loading && (
<span className="text-sm text-gray-400 ml-2">Loading...</span>
)}
{loading && <span className="text-sm text-gray-400 ml-2">Loading...</span>}
</div>
</div>
{collapsible && (
@@ -1,5 +1,5 @@
import { CheckCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline';
import React from 'react';
import { ExclamationTriangleIcon, CheckCircleIcon } from '@heroicons/react/24/outline';
interface ValidatedFieldProps {
label: string;
@@ -28,7 +28,7 @@ const ValidatedField: React.FC<ValidatedFieldProps> = ({
className = '',
fullWidth = false,
validation = 'none',
disabled = false
disabled = false,
}) => {
const hasError = !!error;
const isValid = validation === 'valid' && !hasError && value.trim() !== '';
@@ -37,26 +37,24 @@ const ValidatedField: React.FC<ValidatedFieldProps> = ({
w-full px-4 py-3 rounded-lg border transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black
disabled:opacity-50 disabled:cursor-not-allowed
${hasError
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 placeholder-coral-400/50 focus:border-coral-500 focus:ring-coral-500/30'
: isValid
? 'border-sage-500/60 bg-sage-500/5 text-white placeholder-stone-400 focus:border-sage-500 focus:ring-sage-500/30'
: 'border-stone-800/60 bg-stone-900/40 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-primary-500/30'
${
hasError
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 placeholder-coral-400/50 focus:border-coral-500 focus:ring-coral-500/30'
: isValid
? 'border-sage-500/60 bg-sage-500/5 text-white placeholder-stone-400 focus:border-sage-500 focus:ring-sage-500/30'
: 'border-stone-800/60 bg-stone-900/40 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-primary-500/30'
}
`;
return (
<label className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
<label
className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
<div>
<span className="font-medium">
{label}
{required && <span className="text-coral-400 ml-1">*</span>}
</span>
{helpText && (
<p className="text-xs text-gray-400 leading-relaxed mt-1">
{helpText}
</p>
)}
{helpText && <p className="text-xs text-gray-400 leading-relaxed mt-1">{helpText}</p>}
</div>
<div className="relative">
@@ -65,7 +63,7 @@ const ValidatedField: React.FC<ValidatedFieldProps> = ({
className={inputClasses}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
onChange={e => onChange(e.target.value)}
disabled={disabled}
/>
@@ -119,7 +117,7 @@ const ValidatedSelect: React.FC<ValidatedSelectProps> = ({
className = '',
fullWidth = false,
disabled = false,
validation = 'none'
validation = 'none',
}) => {
const hasError = !!error;
const isValid = validation === 'valid' && !hasError && value.trim() !== '';
@@ -128,35 +126,32 @@ const ValidatedSelect: React.FC<ValidatedSelectProps> = ({
w-full px-4 py-3 rounded-lg border transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black
disabled:opacity-50 disabled:cursor-not-allowed
${hasError
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 focus:border-coral-500 focus:ring-coral-500/30'
: isValid
? 'border-sage-500/60 bg-sage-500/5 text-white focus:border-sage-500 focus:ring-sage-500/30'
: 'border-stone-800/60 bg-stone-900/40 text-white focus:border-primary-500/50 focus:ring-primary-500/30'
${
hasError
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 focus:border-coral-500 focus:ring-coral-500/30'
: isValid
? 'border-sage-500/60 bg-sage-500/5 text-white focus:border-sage-500 focus:ring-sage-500/30'
: 'border-stone-800/60 bg-stone-900/40 text-white focus:border-primary-500/50 focus:ring-primary-500/30'
}
`;
return (
<label className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
<label
className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
<div>
<span className="font-medium">
{label}
{required && <span className="text-coral-400 ml-1">*</span>}
</span>
{helpText && (
<p className="text-xs text-gray-400 leading-relaxed mt-1">
{helpText}
</p>
)}
{helpText && <p className="text-xs text-gray-400 leading-relaxed mt-1">{helpText}</p>}
</div>
<div className="relative">
<select
className={selectClasses}
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
>
onChange={e => onChange(e.target.value)}
disabled={disabled}>
{placeholder && <option value="">{placeholder}</option>}
{options.map(option => (
<option key={option.value} value={option.value}>
@@ -189,4 +184,4 @@ const ValidatedSelect: React.FC<ValidatedSelectProps> = ({
};
export default ValidatedField;
export { ValidatedSelect };
export { ValidatedSelect };
+13 -14
View File
@@ -6,7 +6,6 @@
*/
import { useCallback } from 'react';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
resetConnectionAttempts,
selectDaemonComponents,
@@ -19,10 +18,11 @@ import {
setAutoStartEnabled,
setIsRecovering,
} from '../store/daemonSlice';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
alphahumanServiceStart,
alphahumanServiceStop,
alphahumanServiceStatus,
alphahumanServiceStop,
type CommandResponse,
type ServiceStatus,
} from '../utils/tauriCommands';
@@ -94,14 +94,15 @@ export const useDaemonHealth = (userId?: string) => {
}
}, [dispatch, userId]);
const checkDaemonStatus = useCallback(async (): Promise<CommandResponse<ServiceStatus> | null> => {
try {
return await alphahumanServiceStatus();
} catch (error) {
console.error('[useDaemonHealth] Failed to check daemon status:', error);
return null;
}
}, []);
const checkDaemonStatus =
useCallback(async (): Promise<CommandResponse<ServiceStatus> | null> => {
try {
return await alphahumanServiceStatus();
} catch (error) {
console.error('[useDaemonHealth] Failed to check daemon status:', error);
return null;
}
}, []);
const setAutoStart = useCallback(
(enabled: boolean) => {
@@ -121,9 +122,7 @@ export const useDaemonHealth = (userId?: string) => {
const errorComponentCount = Object.values(components).filter(c => c.status === 'error').length;
// Get uptime in human readable format
const uptimeText = healthSnapshot
? formatUptime(healthSnapshot.uptime_seconds)
: 'Unknown';
const uptimeText = healthSnapshot ? formatUptime(healthSnapshot.uptime_seconds) : 'Unknown';
return {
// State
@@ -195,4 +194,4 @@ export function formatRelativeTime(isoString: string): string {
const days = Math.floor(diffSeconds / 86400);
return `${days}d ago`;
}
}
}
+12 -9
View File
@@ -7,20 +7,20 @@
* - Exponential backoff for restart attempts
* - Error recovery logic
*/
import { useEffect, useRef, useCallback } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
incrementConnectionAttempts,
resetConnectionAttempts,
selectDaemonConnectionAttempts,
selectDaemonStatus,
selectIsDaemonAutoStartEnabled,
selectDaemonConnectionAttempts,
selectIsDaemonRecovering,
setIsRecovering,
} from '../store/daemonSlice';
import { useDaemonHealth } from './useDaemonHealth';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import { isTauri } from '../utils/tauriCommands';
import { useDaemonHealth } from './useDaemonHealth';
// Configuration constants
const MAX_RECONNECTION_ATTEMPTS = 5;
@@ -105,7 +105,9 @@ export const useDaemonLifecycle = (userId?: string) => {
}
const retryDelay = calculateRetryDelay(connectionAttempts + 1);
console.log(`[DaemonLifecycle] Scheduling retry attempt ${connectionAttempts + 1} in ${retryDelay}ms`);
console.log(
`[DaemonLifecycle] Scheduling retry attempt ${connectionAttempts + 1} in ${retryDelay}ms`
);
// Clear existing timeout
if (retryTimeoutRef.current) {
@@ -259,8 +261,9 @@ export const useDaemonLifecycle = (userId?: string) => {
// Config
MAX_RECONNECTION_ATTEMPTS,
nextRetryDelay: connectionAttempts < MAX_RECONNECTION_ATTEMPTS
? calculateRetryDelay(connectionAttempts + 1)
: null,
nextRetryDelay:
connectionAttempts < MAX_RECONNECTION_ATTEMPTS
? calculateRetryDelay(connectionAttempts + 1)
: null,
};
};
};
+1 -2
View File
@@ -4,7 +4,6 @@
* Components using this hook will automatically re-render when TOOLS.md
* is updated and the cache is refreshed.
*/
import { useEffect, useState } from 'react';
interface ToolsUpdateEvent {
@@ -46,4 +45,4 @@ export function useForceToolsRefresh(): () => Promise<void> {
};
return forceRefresh;
}
}
+29 -34
View File
@@ -2,38 +2,29 @@
* Unit tests for the unified AI loader system.
* Tests loading, parallel execution, and error handling.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { loadAIConfig, refreshSoul, refreshTools, refreshAll, clearAICache } from '../loader';
import { clearAICache, loadAIConfig, refreshAll, refreshSoul, refreshTools } from '../loader';
// Import the mocked functions
import { clearSoulCache, loadSoul } from '../soul/loader';
import type { SoulConfig } from '../soul/types';
import { clearToolsCache, loadTools } from '../tools/loader';
import type { ToolsConfig } from '../tools/types';
// Mock the individual loaders
vi.mock('../soul/loader', () => ({
loadSoul: vi.fn(),
clearSoulCache: vi.fn()
}));
vi.mock('../soul/loader', () => ({ loadSoul: vi.fn(), clearSoulCache: vi.fn() }));
vi.mock('../tools/loader', () => ({
loadTools: vi.fn(),
clearToolsCache: vi.fn()
}));
vi.mock('../tools/loader', () => ({ loadTools: vi.fn(), clearToolsCache: vi.fn() }));
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn()
clear: vi.fn(),
};
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
});
// Import the mocked functions
import { loadSoul, clearSoulCache } from '../soul/loader';
import { loadTools, clearToolsCache } from '../tools/loader';
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
describe('Unified AI Loader', () => {
const mockSoulConfig: SoulConfig = {
@@ -47,7 +38,7 @@ describe('Unified AI Loader', () => {
memorySettings: { remember: [] },
emergencyResponses: [],
isDefault: false,
loadedAt: Date.now()
loadedAt: Date.now(),
};
const mockToolsConfig: ToolsConfig = {
@@ -61,10 +52,10 @@ describe('Unified AI Loader', () => {
activeSkills: 0,
categoriesCount: 0,
toolsByCategory: {},
skillsByCategory: {}
skillsByCategory: {},
},
isDefault: false,
loadedAt: Date.now()
loadedAt: Date.now(),
};
beforeEach(() => {
@@ -109,15 +100,11 @@ describe('Unified AI Loader', () => {
loadedAt: Date.now(),
loadingDuration: 100,
hasFallbacks: false,
sources: { soul: 'github', tools: 'github' }
}
sources: { soul: 'github', tools: 'github' },
},
};
const cacheEntry = {
config: cachedConfig,
timestamp: Date.now(),
version: '1.0.0'
};
const cacheEntry = { config: cachedConfig, timestamp: Date.now(), version: '1.0.0' };
localStorageMock.getItem.mockReturnValue(JSON.stringify(cacheEntry));
@@ -182,14 +169,16 @@ describe('Unified AI Loader', () => {
it('should handle timeout correctly', async () => {
// Make loadSoul take a long time
(loadSoul as any).mockImplementation(() =>
new Promise(resolve => setTimeout(() => resolve(mockSoulConfig), 100))
(loadSoul as any).mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve(mockSoulConfig), 100))
);
const config = await loadAIConfig({ timeout: 50 });
expect(config.soul.isDefault).toBe(true); // Should use fallback due to timeout
expect(config.metadata.errors).toContain('Soul loading failed: Error: Operation timed out after 50ms');
expect(config.metadata.errors).toContain(
'Soul loading failed: Error: Operation timed out after 50ms'
);
}, 10000);
});
@@ -200,7 +189,10 @@ describe('Unified AI Loader', () => {
// Reset mocks
vi.clearAllMocks();
const newSoulConfig = { ...mockSoulConfig, identity: { name: 'Updated', description: 'Updated' } };
const newSoulConfig = {
...mockSoulConfig,
identity: { name: 'Updated', description: 'Updated' },
};
(loadSoul as any).mockResolvedValue(newSoulConfig);
const result = await refreshSoul();
@@ -220,7 +212,10 @@ describe('Unified AI Loader', () => {
// Reset mocks
vi.clearAllMocks();
const newToolsConfig = { ...mockToolsConfig, statistics: { ...mockToolsConfig.statistics, totalTools: 10 } };
const newToolsConfig = {
...mockToolsConfig,
statistics: { ...mockToolsConfig.statistics, totalTools: 10 },
};
(loadTools as any).mockResolvedValue(newToolsConfig);
const result = await refreshTools();
@@ -255,4 +250,4 @@ describe('Unified AI Loader', () => {
expect(localStorageMock.removeItem).toHaveBeenCalledWith('alphahuman.ai.cache');
});
});
});
});
+23 -22
View File
@@ -1,20 +1,14 @@
import { loadAIConfig } from './loader';
import type { Message } from './providers/interface';
import { injectSoulIntoMessage } from './soul/injector';
import { injectToolsIntoMessage } from './tools/injector';
import type { Message } from './providers/interface';
import type { AIConfig } from './types';
export interface UnifiedInjectionOptions {
mode: 'prepend' | 'context-block' | 'invisible';
includeMetadata?: boolean;
soul?: {
enabled?: boolean;
};
tools?: {
enabled?: boolean;
maxTools?: number;
format?: 'list' | 'categories' | 'compact';
};
soul?: { enabled?: boolean };
tools?: { enabled?: boolean; maxTools?: number; format?: 'list' | 'categories' | 'compact' };
}
/**
@@ -30,7 +24,12 @@ export async function injectAll(
let config: AIConfig;
let options: UnifiedInjectionOptions;
if (configOrOptions && 'soul' in configOrOptions && 'tools' in configOrOptions && 'metadata' in configOrOptions) {
if (
configOrOptions &&
'soul' in configOrOptions &&
'tools' in configOrOptions &&
'metadata' in configOrOptions
) {
// First param is AIConfig
config = configOrOptions;
options = optionsWhenConfigProvided || { mode: 'context-block' };
@@ -46,7 +45,7 @@ export async function injectAll(
soul: { enabled: true },
tools: { enabled: true, maxTools: 20, format: 'compact' },
...options,
mode: options.mode || 'context-block'
mode: options.mode || 'context-block',
};
let injectedMessage = message;
@@ -56,7 +55,7 @@ export async function injectAll(
try {
injectedMessage = injectSoulIntoMessage(injectedMessage, config.soul, {
mode: finalOptions.mode,
includeMetadata: finalOptions.includeMetadata
includeMetadata: finalOptions.includeMetadata,
});
} catch (error) {
console.warn('⚠️ SOUL injection failed, continuing with TOOLS only:', error);
@@ -70,7 +69,7 @@ export async function injectAll(
mode: finalOptions.mode,
includeMetadata: finalOptions.includeMetadata,
maxTools: finalOptions.tools.maxTools,
format: finalOptions.tools.format
format: finalOptions.tools.format,
});
} catch (error) {
console.warn('⚠️ TOOLS injection failed, continuing without tools context:', error);
@@ -96,11 +95,7 @@ export function hasInjectedContext(message: Message): {
const hasSoul = text.includes('[PERSONA_CONTEXT]') || text.includes('<!--SOUL_CONTEXT:');
const hasTools = text.includes('[TOOLS_CONTEXT]') || text.includes('<!--TOOLS_CONTEXT:');
return {
hasSoul,
hasTools,
hasAny: hasSoul || hasTools
};
return { hasSoul, hasTools, hasAny: hasSoul || hasTools };
}
/**
@@ -125,8 +120,14 @@ export function extractContextInfo(message: Message): {
// Calculate original message length (without injected context)
let originalText = text;
originalText = originalText.replace(/\[PERSONA_CONTEXT\][\s\S]*?\[\/PERSONA_CONTEXT\]\s*User message:\s*/g, '');
originalText = originalText.replace(/\[TOOLS_CONTEXT\][\s\S]*?\[\/TOOLS_CONTEXT\]\s*User message:\s*/g, '');
originalText = originalText.replace(
/\[PERSONA_CONTEXT\][\s\S]*?\[\/PERSONA_CONTEXT\]\s*User message:\s*/g,
''
);
originalText = originalText.replace(
/\[TOOLS_CONTEXT\][\s\S]*?\[\/TOOLS_CONTEXT\]\s*User message:\s*/g,
''
);
originalText = originalText.replace(/<!--SOUL_CONTEXT:[^>]+-->/g, '');
originalText = originalText.replace(/<!--TOOLS_CONTEXT:[^>]+-->/g, '');
@@ -134,6 +135,6 @@ export function extractContextInfo(message: Message): {
soulContextLength,
toolsContextLength,
totalInjectedLength: soulContextLength + toolsContextLength,
originalLength: originalText.length
originalLength: originalText.length,
};
}
}
+27 -43
View File
@@ -4,17 +4,16 @@
* Provides a single interface for loading both SOUL and TOOLS configurations
* with parallel loading, caching, and fallback strategies.
*/
import { loadSoul, clearSoulCache } from './soul/loader';
import { loadTools, clearToolsCache } from './tools/loader';
import { clearSoulCache, loadSoul } from './soul/loader';
import type { SoulConfig } from './soul/types';
import { clearToolsCache, loadTools } from './tools/loader';
import type { ToolsConfig } from './tools/types';
import type {
AIConfig,
AIConfigMetadata,
AIConfigLoadOptions,
AIConfigCacheEntry,
AIConfigLoadResult
AIConfigLoadOptions,
AIConfigLoadResult,
AIConfigMetadata,
} from './types';
const AI_CACHE_KEY = 'alphahuman.ai.cache';
@@ -27,11 +26,7 @@ let cachedAIConfig: AIConfig | null = null;
* Load complete AI configuration (SOUL + TOOLS) with caching and parallel loading
*/
export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<AIConfig> {
const {
forceRefresh = false,
includeMetadata = true,
timeout = 30000
} = options;
const { forceRefresh = false, includeMetadata = true, timeout = 30000 } = options;
const startTime = Date.now();
@@ -46,10 +41,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
const cached = localStorage.getItem(AI_CACHE_KEY);
if (cached) {
const parsed = JSON.parse(cached) as AIConfigCacheEntry;
if (
Date.now() - parsed.timestamp < AI_CACHE_TTL &&
parsed.version === CACHE_VERSION
) {
if (Date.now() - parsed.timestamp < AI_CACHE_TTL && parsed.version === CACHE_VERSION) {
cachedAIConfig = parsed.config;
return parsed.config;
}
@@ -68,7 +60,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
// Load both configurations in parallel
const [soulResult, toolsResult] = await Promise.allSettled([
loadWithTimeout(loadSoul(), timeout),
loadWithTimeout(loadTools(), timeout)
loadWithTimeout(loadTools(), timeout),
]);
// Extract results and handle errors
@@ -98,10 +90,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
loadedAt: endTime,
loadingDuration: endTime - startTime,
hasFallbacks: soul.isDefault || tools.isDefault,
sources: {
soul: getSoulSource(soul),
tools: getToolsSource(tools)
}
sources: { soul: getSoulSource(soul), tools: getToolsSource(tools) },
};
if (errors.length > 0 && includeMetadata) {
@@ -109,11 +98,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
}
// Combine into unified config
const config: AIConfig = {
soul,
tools,
metadata
};
const config: AIConfig = { soul, tools, metadata };
// Cache the result
cachedAIConfig = config;
@@ -121,7 +106,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
const cacheEntry: AIConfigCacheEntry = {
config,
timestamp: Date.now(),
version: CACHE_VERSION
version: CACHE_VERSION,
};
localStorage.setItem(AI_CACHE_KEY, JSON.stringify(cacheEntry));
} catch {
@@ -134,7 +119,9 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
/**
* Load AI configuration with detailed result information
*/
export async function loadAIConfigWithResult(options: AIConfigLoadOptions = {}): Promise<AIConfigLoadResult> {
export async function loadAIConfigWithResult(
options: AIConfigLoadOptions = {}
): Promise<AIConfigLoadResult> {
const startTime = Date.now();
try {
@@ -145,7 +132,7 @@ export async function loadAIConfigWithResult(options: AIConfigLoadOptions = {}):
config,
success: true,
errors: config.metadata.errors || [],
duration: endTime - startTime
duration: endTime - startTime,
};
} catch (error) {
const endTime = Date.now();
@@ -155,7 +142,7 @@ export async function loadAIConfigWithResult(options: AIConfigLoadOptions = {}):
config: createFallbackAIConfig(),
success: false,
errors: [errorMessage],
duration: endTime - startTime
duration: endTime - startTime,
};
}
}
@@ -180,7 +167,7 @@ export async function refreshSoul(): Promise<SoulConfig> {
const cacheEntry: AIConfigCacheEntry = {
config: cachedAIConfig,
timestamp: Date.now(),
version: CACHE_VERSION
version: CACHE_VERSION,
};
localStorage.setItem(AI_CACHE_KEY, JSON.stringify(cacheEntry));
}
@@ -211,7 +198,7 @@ export async function refreshTools(): Promise<ToolsConfig> {
const cacheEntry: AIConfigCacheEntry = {
config: cachedAIConfig,
timestamp: Date.now(),
version: CACHE_VERSION
version: CACHE_VERSION,
};
localStorage.setItem(AI_CACHE_KEY, JSON.stringify(cacheEntry));
}
@@ -265,7 +252,7 @@ async function loadWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promi
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)
)
),
]);
}
@@ -286,7 +273,7 @@ function createFallbackSoulConfig(): SoulConfig {
raw: '# Fallback SOUL Configuration\n\nThis is a fallback configuration used when the main SOUL config cannot be loaded.',
identity: {
name: 'AlphaHuman Assistant',
description: 'AI assistant with fallback configuration'
description: 'AI assistant with fallback configuration',
},
personality: [],
voiceTone: [],
@@ -296,7 +283,7 @@ function createFallbackSoulConfig(): SoulConfig {
memorySettings: { remember: [] },
emergencyResponses: [],
isDefault: true,
loadedAt: Date.now()
loadedAt: Date.now(),
};
}
@@ -312,10 +299,10 @@ function createFallbackToolsConfig(): ToolsConfig {
activeSkills: 0,
categoriesCount: 0,
toolsByCategory: {},
skillsByCategory: {}
skillsByCategory: {},
},
isDefault: true,
loadedAt: Date.now()
loadedAt: Date.now(),
};
}
@@ -330,11 +317,8 @@ function createFallbackAIConfig(): AIConfig {
loadedAt: Date.now(),
loadingDuration: 0,
hasFallbacks: true,
sources: {
soul: 'bundled',
tools: 'bundled'
},
errors: ['Failed to load AI configuration, using fallbacks']
}
sources: { soul: 'bundled', tools: 'bundled' },
errors: ['Failed to load AI configuration, using fallbacks'],
},
};
}
}
+22 -23
View File
@@ -8,10 +8,9 @@ import type { SoulConfig } from './types';
export function injectSoulIntoMessage(
message: Message,
soulConfig: SoulConfig,
options: {
mode: 'prepend' | 'context-block' | 'invisible';
includeMetadata?: boolean;
} = { mode: 'context-block' }
options: { mode: 'prepend' | 'context-block' | 'invisible'; includeMetadata?: boolean } = {
mode: 'context-block',
}
): Message {
if (message.role !== 'user') {
return message; // Only inject into user messages
@@ -21,21 +20,18 @@ export function injectSoulIntoMessage(
switch (options.mode) {
case 'prepend':
return {
...message,
content: [
{ type: 'text', text: soulContext },
...message.content
]
};
return { ...message, content: [{ type: 'text', text: soulContext }, ...message.content] };
case 'context-block':
return {
...message,
content: [
{ type: 'text', text: `[PERSONA_CONTEXT]\n${soulContext}\n[/PERSONA_CONTEXT]\n\nUser message:` },
...message.content
]
{
type: 'text',
text: `[PERSONA_CONTEXT]\n${soulContext}\n[/PERSONA_CONTEXT]\n\nUser message:`,
},
...message.content,
],
};
case 'invisible':
@@ -44,13 +40,10 @@ export function injectSoulIntoMessage(
...message,
content: message.content.map((block, index) => {
if (index === 0 && block.type === 'text') {
return {
...block,
text: `<!--SOUL_CONTEXT:${btoa(soulContext)}-->${block.text}`
};
return { ...block, text: `<!--SOUL_CONTEXT:${btoa(soulContext)}-->${block.text}` };
}
return block;
})
}),
};
default:
@@ -75,7 +68,10 @@ function buildSoulContext(soulConfig: SoulConfig, includeMetadata = false): stri
// Voice guidelines (condensed)
if (soulConfig.voiceTone.length > 0) {
const guidelines = soulConfig.voiceTone.slice(0, 2).map(v => v.guideline).join(', ');
const guidelines = soulConfig.voiceTone
.slice(0, 2)
.map(v => v.guideline)
.join(', ');
parts.push(`Voice: ${guidelines}`);
}
@@ -105,12 +101,15 @@ export function stripSoulFromMessage(message: Message): Message {
content: message.content.map(block => {
if (block.type === 'text') {
// Remove context blocks
let text = block.text.replace(/\[PERSONA_CONTEXT\][\s\S]*?\[\/PERSONA_CONTEXT\]\s*User message:\s*/g, '');
let text = block.text.replace(
/\[PERSONA_CONTEXT\][\s\S]*?\[\/PERSONA_CONTEXT\]\s*User message:\s*/g,
''
);
// Remove invisible context
text = text.replace(/<!--SOUL_CONTEXT:[^>]+-->/g, '');
return { ...block, text };
}
return block;
})
}),
};
}
}
+20 -21
View File
@@ -1,7 +1,18 @@
import soulMd from '../../../../ai/SOUL.md?raw';
import type { SoulConfig, SoulIdentity, PersonalityTrait, VoiceToneGuideline, BehaviorPattern, SafetyRule, Interaction, MemorySettings, EmergencyResponse } from './types';
import type {
BehaviorPattern,
EmergencyResponse,
Interaction,
MemorySettings,
PersonalityTrait,
SafetyRule,
SoulConfig,
SoulIdentity,
VoiceToneGuideline,
} from './types';
const SOUL_GITHUB_URL = 'https://raw.githubusercontent.com/alphahumanxyz/alphahuman/refs/heads/main/ai/SOUL.md';
const SOUL_GITHUB_URL =
'https://raw.githubusercontent.com/alphahumanxyz/alphahuman/refs/heads/main/ai/SOUL.md';
const SOUL_CACHE_KEY = 'alphahuman.soul.cache';
const SOUL_CACHE_TTL = 1000 * 60 * 30; // 30 minutes
@@ -53,10 +64,7 @@ export async function loadSoul(): Promise<SoulConfig> {
// Cache the result
cachedSoulConfig = config;
try {
localStorage.setItem(SOUL_CACHE_KEY, JSON.stringify({
config,
timestamp: Date.now()
}));
localStorage.setItem(SOUL_CACHE_KEY, JSON.stringify({ config, timestamp: Date.now() }));
} catch {
// Ignore storage errors
}
@@ -88,7 +96,7 @@ export function parseSoul(raw: string, isDefault: boolean): SoulConfig {
memorySettings,
emergencyResponses,
isDefault,
loadedAt: Date.now()
loadedAt: Date.now(),
};
}
@@ -127,10 +135,7 @@ function parsePersonality(raw: string): PersonalityTrait[] {
for (const line of lines) {
const match = line.match(/- \*\*(.+?)\*\*:\s*(.+)/);
if (match) {
traits.push({
trait: match[1].trim(),
description: match[2].trim()
});
traits.push({ trait: match[1].trim(), description: match[2].trim() });
}
}
@@ -194,7 +199,7 @@ function parseSafetyRules(raw: string): SafetyRule[] {
rules.push({
id: `safety-${i + 1}`,
rule,
priority: 10 - i // Earlier rules have higher priority
priority: 10 - i, // Earlier rules have higher priority
});
}
}
@@ -212,10 +217,7 @@ function parseInteractions(raw: string): Interaction[] {
for (const line of lines) {
const match = line.match(/^\d+\.\s*\*\*(.+?)\*\*:\s*(.+)/);
if (match) {
interactions.push({
name: match[1].trim(),
description: match[2].trim()
});
interactions.push({ name: match[1].trim(), description: match[2].trim() });
}
}
@@ -249,10 +251,7 @@ function parseEmergencyResponses(raw: string): EmergencyResponse[] {
for (const line of lines) {
const match = line.match(/- \*\*(.+?)\*\*:\s*(.+)/);
if (match) {
responses.push({
trigger: match[1].trim(),
response: match[2].trim()
});
responses.push({ trigger: match[1].trim(), response: match[2].trim() });
}
}
@@ -269,4 +268,4 @@ export function clearSoulCache(): void {
} catch {
// Ignore storage errors
}
}
}
+1 -1
View File
@@ -61,4 +61,4 @@ export interface MemorySettings {
export interface EmergencyResponse {
trigger: string;
response: string;
}
}
+15 -27
View File
@@ -2,9 +2,9 @@
* Unit tests for the tools loader system.
* Tests loading, parsing, and caching functionality.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { loadTools, parseTools, clearToolsCache } from '../loader';
import { clearToolsCache, loadTools, parseTools } from '../loader';
import type { ToolsConfig } from '../types';
// Mock the bundled tools markdown
@@ -54,7 +54,7 @@ AlphaHuman has access to **4 tools** across **2 integrations**.
- **to** (string) **(required)**: Recipient email address
- **subject** (string) **(required)**: Email subject line
- **body** (string) **(required)**: Email body content
`
`,
}));
// Mock localStorage
@@ -62,12 +62,10 @@ const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn()
clear: vi.fn(),
};
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
});
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
// Mock fetch
global.fetch = vi.fn();
@@ -110,7 +108,7 @@ describe('Tools Loader', () => {
expect(config.tools[0]).toMatchObject({
name: 'send_message',
description: 'Send a message to a Telegram chat',
skillId: 'telegram'
skillId: 'telegram',
});
expect(config.tools[0].inputSchema.properties).toHaveProperty('chat_id');
@@ -121,7 +119,7 @@ describe('Tools Loader', () => {
expect(config.tools[1]).toMatchObject({
name: 'get_history',
description: 'Get chat history',
skillId: 'telegram'
skillId: 'telegram',
});
expect(config.tools[1].inputSchema.properties).toEqual({});
@@ -206,17 +204,13 @@ describe('Tools Loader', () => {
activeSkills: 0,
categoriesCount: 0,
toolsByCategory: {},
skillsByCategory: {}
skillsByCategory: {},
},
isDefault: false,
loadedAt: Date.now()
loadedAt: Date.now(),
};
const cacheEntry = {
config: mockConfig,
timestamp: Date.now(),
version: '1.0.0'
};
const cacheEntry = { config: mockConfig, timestamp: Date.now(), version: '1.0.0' };
localStorageMock.getItem.mockReturnValue(JSON.stringify(cacheEntry));
@@ -229,16 +223,13 @@ describe('Tools Loader', () => {
it('should load from GitHub when cache is expired', async () => {
const expiredCacheEntry = {
config: {} as ToolsConfig,
timestamp: Date.now() - (1000 * 60 * 60), // 1 hour ago
version: '1.0.0'
timestamp: Date.now() - 1000 * 60 * 60, // 1 hour ago
version: '1.0.0',
};
localStorageMock.getItem.mockReturnValue(JSON.stringify(expiredCacheEntry));
(fetch as any).mockResolvedValue({
ok: true,
text: () => Promise.resolve('# Remote Tools')
});
(fetch as any).mockResolvedValue({ ok: true, text: () => Promise.resolve('# Remote Tools') });
const result = await loadTools();
@@ -263,10 +254,7 @@ describe('Tools Loader', () => {
it('should cache the loaded configuration', async () => {
localStorageMock.getItem.mockReturnValue(null);
(fetch as any).mockResolvedValue({
ok: true,
text: () => Promise.resolve('# Remote Tools')
});
(fetch as any).mockResolvedValue({ ok: true, text: () => Promise.resolve('# Remote Tools') });
await loadTools();
@@ -292,4 +280,4 @@ describe('Tools Loader', () => {
expect(() => clearToolsCache()).not.toThrow();
});
});
});
});
+19 -20
View File
@@ -25,21 +25,18 @@ export function injectToolsIntoMessage(
switch (options.mode) {
case 'prepend':
return {
...message,
content: [
{ type: 'text', text: toolsContext },
...message.content
]
};
return { ...message, content: [{ type: 'text', text: toolsContext }, ...message.content] };
case 'context-block':
return {
...message,
content: [
{ type: 'text', text: `[TOOLS_CONTEXT]\n${toolsContext}\n[/TOOLS_CONTEXT]\n\nUser message:` },
...message.content
]
{
type: 'text',
text: `[TOOLS_CONTEXT]\n${toolsContext}\n[/TOOLS_CONTEXT]\n\nUser message:`,
},
...message.content,
],
};
case 'invisible':
@@ -48,13 +45,10 @@ export function injectToolsIntoMessage(
...message,
content: message.content.map((block, index) => {
if (index === 0 && block.type === 'text') {
return {
...block,
text: `<!--TOOLS_CONTEXT:${btoa(toolsContext)}-->${block.text}`
};
return { ...block, text: `<!--TOOLS_CONTEXT:${btoa(toolsContext)}-->${block.text}` };
}
return block;
})
}),
};
default:
@@ -69,11 +63,13 @@ function buildToolsContext(toolsConfig: ToolsConfig, options: ToolsInjectionOpti
const parts: string[] = [];
// Compact format - statistics and key info
parts.push(`${toolsConfig.statistics.totalTools} tools across ${toolsConfig.statistics.activeSkills} skills`);
parts.push(
`${toolsConfig.statistics.totalTools} tools across ${toolsConfig.statistics.activeSkills} skills`
);
// Top categories by tool count
const topCategories = Object.entries(toolsConfig.statistics.toolsByCategory)
.sort(([,a], [,b]) => b - a)
.sort(([, a], [, b]) => b - a)
.slice(0, 4)
.map(([cat, count]) => `${toolsConfig.categories[cat]?.name || cat} (${count})`)
.join(', ');
@@ -119,12 +115,15 @@ export function stripToolsFromMessage(message: Message): Message {
content: message.content.map(block => {
if (block.type === 'text') {
// Remove context blocks
let text = block.text.replace(/\[TOOLS_CONTEXT\][\s\S]*?\[\/TOOLS_CONTEXT\]\s*User message:\s*/g, '');
let text = block.text.replace(
/\[TOOLS_CONTEXT\][\s\S]*?\[\/TOOLS_CONTEXT\]\s*User message:\s*/g,
''
);
// Remove invisible context
text = text.replace(/<!--TOOLS_CONTEXT:[^>]+-->/g, '');
return { ...block, text };
}
return block;
})
}),
};
}
}
+40 -52
View File
@@ -1,13 +1,13 @@
import toolsMd from '../../../../ai/TOOLS.md?raw';
import type {
ToolsConfig,
ToolDefinition,
SkillGroup,
ToolCategory,
ToolDefinition,
ToolEnvironment,
ToolStatistics,
ToolParseResult,
ToolsCacheEntry
ToolsCacheEntry,
ToolsConfig,
ToolStatistics,
} from './types';
// GitHub URL removed - always use bundled file updated by auto-update system
@@ -34,10 +34,7 @@ export async function loadTools(): Promise<ToolsConfig> {
const cached = localStorage.getItem(TOOLS_CACHE_KEY);
if (cached) {
const parsed = JSON.parse(cached) as ToolsCacheEntry;
if (
Date.now() - parsed.timestamp < TOOLS_CACHE_TTL &&
parsed.version === CACHE_VERSION
) {
if (Date.now() - parsed.timestamp < TOOLS_CACHE_TTL && parsed.version === CACHE_VERSION) {
cachedToolsConfig = parsed.config;
return parsed.config;
}
@@ -55,11 +52,7 @@ export async function loadTools(): Promise<ToolsConfig> {
// Cache the result
cachedToolsConfig = config;
try {
const cacheEntry: ToolsCacheEntry = {
config,
timestamp: Date.now(),
version: CACHE_VERSION
};
const cacheEntry: ToolsCacheEntry = { config, timestamp: Date.now(), version: CACHE_VERSION };
localStorage.setItem(TOOLS_CACHE_KEY, JSON.stringify(cacheEntry));
} catch {
// Ignore storage errors
@@ -86,7 +79,7 @@ export function parseTools(raw: string, isDefault: boolean): ToolsConfig {
environments,
statistics,
isDefault,
loadedAt: Date.now()
loadedAt: Date.now(),
};
}
@@ -111,7 +104,9 @@ function parseToolsFromMarkdown(raw: string): ToolParseResult {
warnings.push(...sectionTools.warnings);
}
} catch (error) {
errors.push(`Failed to parse tools markdown: ${error instanceof Error ? error.message : 'Unknown error'}`);
errors.push(
`Failed to parse tools markdown: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
return { tools, errors, warnings };
@@ -146,7 +141,9 @@ function parseToolsSection(section: string, sectionIndex: number): ToolParseResu
tools.push(tool);
}
} catch (error) {
errors.push(`Error parsing tool in section ${sectionIndex}, block ${j}: ${error instanceof Error ? error.message : 'Unknown error'}`);
errors.push(
`Error parsing tool in section ${sectionIndex}, block ${j}: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
@@ -174,23 +171,14 @@ function parseToolBlock(block: string, defaultSkillId: string): ToolDefinition |
// Try to determine actual skillId from tool name or description
const skillId = inferSkillIdFromTool(name, description, defaultSkillId);
return {
skillId,
name,
description,
inputSchema
};
return { skillId, name, description, inputSchema };
}
/**
* Parse parameters section to JSON Schema
*/
function parseParametersToSchema(parametersText: string): ToolDefinition['inputSchema'] {
const schema: ToolDefinition['inputSchema'] = {
type: 'object',
properties: {},
required: []
};
const schema: ToolDefinition['inputSchema'] = { type: 'object', properties: {}, required: [] };
if (!parametersText || parametersText.includes('*None*')) {
return schema;
@@ -205,7 +193,7 @@ function parseParametersToSchema(parametersText: string): ToolDefinition['inputS
schema.properties[paramName] = {
type: paramType === 'any' ? 'string' : paramType,
description: paramDescription.trim()
description: paramDescription.trim(),
};
if (isRequired) {
@@ -231,7 +219,7 @@ function groupToolsBySkill(tools: ToolDefinition[]): Record<string, SkillGroup>
skillId,
name: formatSkillName(skillId),
category: categorizeSkill(skillId),
tools: []
tools: [],
};
}
@@ -253,49 +241,49 @@ function generateCategories(skillGroups: Record<string, SkillGroup>): Record<str
id: 'communication',
name: 'Communication',
description: 'Tools for messaging, email, and social interaction',
skills: ['telegram', 'gmail', 'discord', 'slack']
skills: ['telegram', 'gmail', 'discord', 'slack'],
},
productivity: {
id: 'productivity',
name: 'Productivity',
description: 'Tools for task management, note-taking, and organization',
skills: ['notion', 'todoist', 'calendar', 'trello']
skills: ['notion', 'todoist', 'calendar', 'trello'],
},
automation: {
id: 'automation',
name: 'Automation',
description: 'Tools for workflow automation and task scheduling',
skills: ['zapier', 'ifttt', 'scheduler', 'webhook']
skills: ['zapier', 'ifttt', 'scheduler', 'webhook'],
},
data: {
id: 'data',
name: 'Data & Analytics',
description: 'Tools for data processing, analysis, and storage',
skills: ['database', 'csv', 'json', 'analytics']
skills: ['database', 'csv', 'json', 'analytics'],
},
media: {
id: 'media',
name: 'Media & Content',
description: 'Tools for image, video, and content processing',
skills: ['image', 'video', 'audio', 'pdf']
skills: ['image', 'video', 'audio', 'pdf'],
},
utility: {
id: 'utility',
name: 'Utilities',
description: 'General-purpose utility tools and helpers',
skills: ['file', 'text', 'crypto', 'converter']
}
skills: ['file', 'text', 'crypto', 'converter'],
},
};
// Initialize categories with actual skill data
for (const [categoryId, categoryConfig] of Object.entries(predefinedCategories)) {
const skillsInCategory = Object.values(skillGroups).filter(group =>
group.category === categoryId
const skillsInCategory = Object.values(skillGroups).filter(
group => group.category === categoryId
);
categories[categoryId] = {
...categoryConfig,
toolCount: skillsInCategory.reduce((sum, group) => sum + group.tools.length, 0)
toolCount: skillsInCategory.reduce((sum, group) => sum + group.tools.length, 0),
};
}
@@ -330,7 +318,7 @@ function parseEnvironments(raw: string): Record<string, ToolEnvironment> {
accessLevel: extractListItem(envContent, 'Access Level'),
rateLimits: extractListItem(envContent, 'Rate Limits'),
authentication: extractListItem(envContent, 'Authentication'),
logging: extractListItem(envContent, 'Logging')
logging: extractListItem(envContent, 'Logging'),
};
}
@@ -364,7 +352,7 @@ function generateStatistics(
activeSkills: Object.keys(skillGroups).length,
categoriesCount: Object.keys(categories).length,
toolsByCategory,
skillsByCategory
skillsByCategory,
};
}
@@ -416,7 +404,7 @@ function categorizeSkill(skillId: string): string {
image: 'media',
video: 'media',
audio: 'media',
pdf: 'media'
pdf: 'media',
};
for (const [skill, category] of Object.entries(categoryMap)) {
@@ -430,11 +418,11 @@ function categorizeSkill(skillId: string): string {
function inferSkillIdFromCategory(category: string): string {
const categorySkillMap: Record<string, string> = {
'Communication': 'telegram',
'Productivity': 'notion',
'Automation': 'zapier',
Communication: 'telegram',
Productivity: 'notion',
Automation: 'zapier',
'Data & Analytics': 'database',
'Media & Content': 'image'
'Media & Content': 'image',
};
return categorySkillMap[category] || 'unknown';
@@ -453,7 +441,7 @@ function inferSkillIdFromTool(name: string, description: string, defaultSkillId:
calendar: ['calendar', 'event'],
trello: ['trello', 'board'],
zapier: ['zapier'],
ifttt: ['ifttt']
ifttt: ['ifttt'],
};
for (const [skillId, keywords] of Object.entries(skillKeywords)) {
@@ -474,7 +462,7 @@ function getDefaultEnvironments(): Record<string, ToolEnvironment> {
accessLevel: 'Full access to all tools',
rateLimits: 'Relaxed for testing',
authentication: 'Development credentials',
logging: 'Verbose logging enabled'
logging: 'Verbose logging enabled',
},
production: {
id: 'production',
@@ -483,7 +471,7 @@ function getDefaultEnvironments(): Record<string, ToolEnvironment> {
accessLevel: 'Production-safe tools only',
rateLimits: 'Standard API limits enforced',
authentication: 'Production credentials required',
logging: 'Essential logs only'
logging: 'Essential logs only',
},
testing: {
id: 'testing',
@@ -492,8 +480,8 @@ function getDefaultEnvironments(): Record<string, ToolEnvironment> {
accessLevel: 'Safe tools for automated testing',
rateLimits: 'Testing-specific limits',
authentication: 'Test credentials',
logging: 'Test execution logs'
}
logging: 'Test execution logs',
},
};
}
@@ -507,4 +495,4 @@ export function clearToolsCache(): void {
} catch {
// Ignore storage errors
}
}
}
+2 -6
View File
@@ -12,11 +12,7 @@ export interface ToolDefinition {
/** Tool description for AI understanding */
description: string;
/** JSON Schema for input validation */
inputSchema: {
type: "object";
properties: Record<string, unknown>;
required?: string[];
};
inputSchema: { type: 'object'; properties: Record<string, unknown>; required?: string[] };
}
/** Tool category for organization */
@@ -130,4 +126,4 @@ export interface ToolDiscoveryResult {
discoveredAt: number;
/** Any errors during discovery */
errors?: string[];
}
}
+1 -2
View File
@@ -2,7 +2,6 @@
* Unified types for the AI configuration system.
* Combines SOUL persona and TOOLS configurations.
*/
import type { SoulConfig } from './soul/types';
import type { ToolsConfig } from './tools/types';
@@ -63,4 +62,4 @@ export interface AIConfigLoadResult {
errors: string[];
/** Loading duration */
duration: number;
}
}
+1 -6
View File
@@ -34,12 +34,7 @@ export function syncNotionMetadataToBackend(
avatar_url: profile.avatar_url ?? null,
};
const payload = {
requestId: crypto.randomUUID(),
provider: PROVIDER_NOTION,
metadata,
};
const payload = { requestId: crypto.randomUUID(), provider: PROVIDER_NOTION, metadata };
void emitViaRustSocket(INTEGRATION_METADATA_SYNC_EVENT, payload);
}
+20 -20
View File
@@ -4,8 +4,8 @@
* This module provides functionality to automatically update the TOOLS.md file
* with all tools discovered from the running skills system.
*/
import { invoke } from '@tauri-apps/api/core';
import { clearToolsCache } from '../ai/tools/loader';
import { forceToolsCacheRefresh } from './file-watcher';
@@ -18,11 +18,7 @@ interface RuntimeToolResponse {
tool: {
name: string;
description: string;
input_schema: {
type: string;
properties: Record<string, any>;
required?: string[];
};
input_schema: { type: string; properties: Record<string, any>; required?: string[] };
};
}
@@ -68,7 +64,10 @@ export async function updateToolsDocumentation(): Promise<void> {
// Transform the tools data into grouped format
console.log('🔍 Step 4: Grouping tools by skill...');
const toolsBySkill = groupToolsBySkill(toolsResponse);
console.log('🔧 Tools by skill:', Object.keys(toolsBySkill).map(skillId => `${skillId}: ${toolsBySkill[skillId].length} tools`));
console.log(
'🔧 Tools by skill:',
Object.keys(toolsBySkill).map(skillId => `${skillId}: ${toolsBySkill[skillId].length} tools`)
);
// Generate the TOOLS.md content
console.log('🔍 Step 5: Generating TOOLS.md markdown content...');
@@ -83,7 +82,7 @@ export async function updateToolsDocumentation(): Promise<void> {
try {
const writeResult = await invoke('write_ai_config_file', {
filename: 'TOOLS.md',
content: markdownContent
content: markdownContent,
});
console.log('🔧 Write command result:', writeResult);
} catch (writeError) {
@@ -101,11 +100,11 @@ export async function updateToolsDocumentation(): Promise<void> {
// Manually dispatch tools-updated event to ensure UI updates
console.log('📡 Dispatching tools-updated event for UI components...');
window.dispatchEvent(new CustomEvent('tools-updated', {
detail: { timestamp: Date.now() }
}));
window.dispatchEvent(new CustomEvent('tools-updated', { detail: { timestamp: Date.now() } }));
console.log(`📄 Final result: ${toolsResponse.length} tools from ${Object.keys(toolsBySkill).length} skills`);
console.log(
`📄 Final result: ${toolsResponse.length} tools from ${Object.keys(toolsBySkill).length} skills`
);
console.log('=== AUTO-UPDATE TOOLS.md COMPLETE ===');
} catch (error) {
console.error('❌ FATAL ERROR in updateToolsDocumentation:', error);
@@ -117,7 +116,9 @@ export async function updateToolsDocumentation(): Promise<void> {
/**
* Group tools by skill for organized documentation
*/
function groupToolsBySkill(toolsResponse: RuntimeToolResponse[]): Record<string, RuntimeToolResponse[]> {
function groupToolsBySkill(
toolsResponse: RuntimeToolResponse[]
): Record<string, RuntimeToolResponse[]> {
const grouped: Record<string, RuntimeToolResponse[]> = {};
for (const toolResponse of toolsResponse) {
@@ -138,7 +139,8 @@ function generateToolsMarkdown(toolsBySkill: Record<string, RuntimeToolResponse[
const totalTools = Object.values(toolsBySkill).flat().length;
const skillCount = Object.keys(toolsBySkill).length;
const content = [`# AlphaHuman Tools
const content = [
`# AlphaHuman Tools
This document lists all available tools that AlphaHuman can use to interact with external services and perform actions. Tools are organized by integration and automatically updated when the app loads.
@@ -152,7 +154,8 @@ ${Object.entries(toolsBySkill)
.join('\n')}
## Available Tools
`];
`,
];
// Generate tool documentation for each skill
for (const [skillId, tools] of Object.entries(toolsBySkill)) {
@@ -192,10 +195,7 @@ ${Object.entries(toolsBySkill)
content.push('**Example**:');
content.push('```json');
content.push(JSON.stringify({
tool: tool.name,
parameters: exampleParams
}, null, 2));
content.push(JSON.stringify({ tool: tool.name, parameters: exampleParams }, null, 2));
content.push('```\n');
content.push('---\n');
@@ -260,4 +260,4 @@ function getExampleValue(paramName: string, type: string): any {
default:
return `example_${paramName}`;
}
}
}
+6 -7
View File
@@ -4,9 +4,8 @@
* This module sets up automatic cache invalidation whenever the bundled TOOLS.md
* file is updated, ensuring the UI reflects the latest tool data immediately.
*/
import { clearToolsCache } from '../ai/tools/loader';
import { clearAICache, loadAIConfig } from '../ai/loader';
import { clearToolsCache } from '../ai/tools/loader';
// Track file modification time to detect changes
let lastModifiedTime: number | null = null;
@@ -71,9 +70,9 @@ async function checkForToolsChanges(): Promise<void> {
console.log('✅ AI configuration cache refreshed successfully');
// Dispatch custom event for components to react to
window.dispatchEvent(new CustomEvent('tools-updated', {
detail: { timestamp: Date.now() }
}));
window.dispatchEvent(
new CustomEvent('tools-updated', { detail: { timestamp: Date.now() } })
);
} catch (error) {
console.error('❌ Failed to reload AI config after file change:', error);
}
@@ -91,7 +90,7 @@ function simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash;
@@ -106,4 +105,4 @@ export function forceToolsCacheRefresh(): Promise<void> {
clearAICache();
lastModifiedTime = null; // Reset to trigger next check
return loadAIConfig();
}
}
+1 -1
View File
@@ -5,10 +5,10 @@ import ReactDOM from 'react-dom/client';
import App from './App';
import ErrorReportNotification from './components/ErrorReportNotification';
import './index.css';
import { startToolsFileWatcher } from './lib/tools/file-watcher';
import './polyfills';
import { initSentry } from './services/analytics';
import { setupDesktopDeepLinkListener } from './utils/desktopDeepLinkListener';
import { startToolsFileWatcher } from './lib/tools/file-watcher';
// Initialize Sentry early (before React renders)
initSentry();
+1 -5
View File
@@ -421,11 +421,7 @@ const Conversations = () => {
for (let i = 0; i < message.tool_calls.length; i++) {
const tc = message.tool_calls[i];
if (i !== latestIndex) {
loopMessages.push({
role: 'tool',
tool_call_id: tc.id,
content: '',
});
loopMessages.push({ role: 'tool', tool_call_id: tc.id, content: '' });
continue;
}
+1 -1
View File
@@ -14,9 +14,9 @@ import SkillSetupModal from '../components/skills/SkillSetupModal';
import { useIntelligenceStats } from '../hooks/useIntelligenceStats';
import { deriveConnectionStatus, useSkillConnectionStatus } from '../lib/skills/hooks';
import { skillManager } from '../lib/skills/manager';
import type { SkillConnectionStatus, SkillHostConnectionState } from '../lib/skills/types';
import { updateToolsDocumentation } from '../lib/tools/auto-update';
import { forceToolsCacheRefresh } from '../lib/tools/file-watcher';
import type { SkillConnectionStatus, SkillHostConnectionState } from '../lib/skills/types';
import { useAppSelector } from '../store/hooks';
import { IS_DEV } from '../utils/config';
+13 -3
View File
@@ -57,7 +57,12 @@ const SocketProvider = ({ children }: { children: React.ReactNode }) => {
// Setup Tauri event listeners once
useEffect(() => {
console.log('[SocketProvider] useEffect triggered, usesRustSocket:', usesRustSocket, 'tauriListenersSetup:', tauriListenersSetup.current);
console.log(
'[SocketProvider] useEffect triggered, usesRustSocket:',
usesRustSocket,
'tauriListenersSetup:',
tauriListenersSetup.current
);
if (usesRustSocket && !tauriListenersSetup.current) {
console.log('[SocketProvider] Condition met - calling setupTauriSocketListeners()');
@@ -70,7 +75,7 @@ const SocketProvider = ({ children }: { children: React.ReactNode }) => {
.then(() => {
console.log('[SocketProvider] setupTauriSocketListeners() completed successfully');
})
.catch((error) => {
.catch(error => {
console.error('[SocketProvider] setupTauriSocketListeners() failed:', error);
console.error('[SocketProvider] Error details:', {
message: error?.message,
@@ -85,7 +90,12 @@ const SocketProvider = ({ children }: { children: React.ReactNode }) => {
} else if (!usesRustSocket) {
console.log('[SocketProvider] Not using Rust socket, skipping Tauri listener setup');
} else {
console.log('[SocketProvider] Unexpected condition - usesRustSocket:', usesRustSocket, 'tauriListenersSetup.current:', tauriListenersSetup.current);
console.log(
'[SocketProvider] Unexpected condition - usesRustSocket:',
usesRustSocket,
'tauriListenersSetup.current:',
tauriListenersSetup.current
);
}
return () => {
+199 -204
View File
@@ -1,13 +1,14 @@
import { describe, test, expect, beforeEach, vi, type Mock } from 'vitest';
import { beforeEach, describe, expect, type Mock, test, vi } from 'vitest';
import type {
AgentExecutionOptions,
AgentExecutionResult,
AgentToolExecution,
AgentToolSchema,
} from '../../types/agent';
import { AgentLoopService } from '../agentLoop';
import { AgentToolRegistry } from '../agentToolRegistry';
import { apiClient } from '../apiClient';
import type {
AgentToolSchema,
AgentExecutionResult,
AgentToolExecution,
AgentExecutionOptions
} from '../../types/agent';
// Mock dependencies
vi.mock('../agentToolRegistry');
@@ -20,35 +21,35 @@ describe('AgentLoopService', () => {
const mockToolSchemas: AgentToolSchema[] = [
{
type: "function",
type: 'function',
function: {
name: "github_list_issues",
description: "List GitHub issues for a repository",
name: 'github_list_issues',
description: 'List GitHub issues for a repository',
parameters: {
type: "object",
type: 'object',
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" }
owner: { type: 'string', description: 'Repository owner' },
repo: { type: 'string', description: 'Repository name' },
},
required: ["owner", "repo"]
}
}
required: ['owner', 'repo'],
},
},
},
{
type: "function",
type: 'function',
function: {
name: "notion_create_page",
description: "Create a new Notion page",
name: 'notion_create_page',
description: 'Create a new Notion page',
parameters: {
type: "object",
type: 'object',
properties: {
title: { type: "string", description: "Page title" },
content: { type: "string", description: "Page content" }
title: { type: 'string', description: 'Page title' },
content: { type: 'string', description: 'Page content' },
},
required: ["title"]
}
}
}
required: ['title'],
},
},
},
];
beforeEach(() => {
@@ -56,10 +57,7 @@ describe('AgentLoopService', () => {
vi.clearAllMocks();
// Setup default mock implementations
const mockRegistryInstance = {
loadToolSchemas: vi.fn(),
executeTool: vi.fn()
};
const mockRegistryInstance = { loadToolSchemas: vi.fn(), executeTool: vi.fn() };
mockToolRegistry.getInstance.mockReturnValue(mockRegistryInstance as any);
mockRegistryInstance.loadToolSchemas.mockResolvedValue(mockToolSchemas);
@@ -68,28 +66,25 @@ describe('AgentLoopService', () => {
describe('executeTask', () => {
test('should execute simple task without tool calls', async () => {
const mockResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: 'Hello! How can I help you today?',
tool_calls: undefined
choices: [
{
message: {
role: 'assistant' as const,
content: 'Hello! How can I help you today?',
tool_calls: undefined,
},
finish_reason: 'stop' as const,
},
finish_reason: 'stop' as const
}],
usage: {
prompt_tokens: 20,
completion_tokens: 10,
total_tokens: 30
}
],
usage: { prompt_tokens: 20, completion_tokens: 10, total_tokens: 30 },
};
mockApiClient.post.mockResolvedValue({ data: mockResponse });
const result = await service.executeTask(
'Hello',
'conv_123',
{ maxIterations: 5, timeoutMs: 30000 }
);
const result = await service.executeTask('Hello', 'conv_123', {
maxIterations: 5,
timeoutMs: 30000,
});
expect(result.status).toBe('completed');
expect(result.finalResponse).toBe('Hello! How can I help you today?');
@@ -103,50 +98,49 @@ describe('AgentLoopService', () => {
expect.objectContaining({
model: expect.any(String),
messages: expect.arrayContaining([
expect.objectContaining({
role: 'user',
content: 'Hello'
})
expect.objectContaining({ role: 'user', content: 'Hello' }),
]),
tools: mockToolSchemas,
tool_choice: 'auto'
tool_choice: 'auto',
})
);
});
test('should execute task with single tool call', async () => {
const mockToolCallResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: null,
tool_calls: [{
id: 'call_123',
type: 'function' as const,
function: {
name: 'github_list_issues',
arguments: '{"owner":"user","repo":"test"}'
}
}]
choices: [
{
message: {
role: 'assistant' as const,
content: null,
tool_calls: [
{
id: 'call_123',
type: 'function' as const,
function: {
name: 'github_list_issues',
arguments: '{"owner":"user","repo":"test"}',
},
},
],
},
finish_reason: 'tool_calls' as const,
},
finish_reason: 'tool_calls' as const
}]
],
};
const mockFinalResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: 'I found 3 open issues in your repository.',
tool_calls: undefined
choices: [
{
message: {
role: 'assistant' as const,
content: 'I found 3 open issues in your repository.',
tool_calls: undefined,
},
finish_reason: 'stop' as const,
},
finish_reason: 'stop' as const
}],
usage: {
prompt_tokens: 50,
completion_tokens: 20,
total_tokens: 70
}
],
usage: { prompt_tokens: 50, completion_tokens: 20, total_tokens: 70 },
};
const mockToolExecution: AgentToolExecution = {
@@ -158,7 +152,7 @@ describe('AgentLoopService', () => {
startTime: Date.now() - 1500,
endTime: Date.now(),
executionTimeMs: 1500,
result: '{"issues":[{"title":"Bug fix","number":1}]}'
result: '{"issues":[{"title":"Bug fix","number":1}]}',
};
// Setup mocks
@@ -169,10 +163,7 @@ describe('AgentLoopService', () => {
.mockResolvedValueOnce({ data: mockToolCallResponse })
.mockResolvedValueOnce({ data: mockFinalResponse });
const result = await service.executeTask(
'Show me GitHub issues',
'conv_123'
);
const result = await service.executeTask('Show me GitHub issues', 'conv_123');
expect(result.status).toBe('completed');
expect(result.finalResponse).toBe('I found 3 open issues in your repository.');
@@ -191,50 +182,57 @@ describe('AgentLoopService', () => {
test('should handle multiple tool calls in sequence', async () => {
const mockFirstToolCallResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: null,
tool_calls: [{
id: 'call_1',
type: 'function' as const,
function: {
name: 'github_list_issues',
arguments: '{"owner":"user","repo":"test"}'
}
}]
choices: [
{
message: {
role: 'assistant' as const,
content: null,
tool_calls: [
{
id: 'call_1',
type: 'function' as const,
function: {
name: 'github_list_issues',
arguments: '{"owner":"user","repo":"test"}',
},
},
],
},
finish_reason: 'tool_calls' as const,
},
finish_reason: 'tool_calls' as const
}]
],
};
const mockSecondToolCallResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: null,
tool_calls: [{
id: 'call_2',
type: 'function' as const,
function: {
name: 'notion_create_page',
arguments: '{"title":"Issues Summary"}'
}
}]
choices: [
{
message: {
role: 'assistant' as const,
content: null,
tool_calls: [
{
id: 'call_2',
type: 'function' as const,
function: { name: 'notion_create_page', arguments: '{"title":"Issues Summary"}' },
},
],
},
finish_reason: 'tool_calls' as const,
},
finish_reason: 'tool_calls' as const
}]
],
};
const mockFinalResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: 'I created a summary page with your GitHub issues.',
tool_calls: undefined
choices: [
{
message: {
role: 'assistant' as const,
content: 'I created a summary page with your GitHub issues.',
tool_calls: undefined,
},
finish_reason: 'stop' as const,
},
finish_reason: 'stop' as const
}]
],
};
const mockToolExecution1: AgentToolExecution = {
@@ -246,7 +244,7 @@ describe('AgentLoopService', () => {
startTime: Date.now() - 2000,
endTime: Date.now() - 1000,
executionTimeMs: 1000,
result: '{"issues":[{"title":"Bug fix","number":1}]}'
result: '{"issues":[{"title":"Bug fix","number":1}]}',
};
const mockToolExecution2: AgentToolExecution = {
@@ -258,7 +256,7 @@ describe('AgentLoopService', () => {
startTime: Date.now() - 800,
endTime: Date.now(),
executionTimeMs: 800,
result: '{"page_id":"page_123"}'
result: '{"page_id":"page_123"}',
};
// Setup mocks
@@ -300,17 +298,21 @@ describe('AgentLoopService', () => {
test('should respect maximum iterations limit', async () => {
const mockToolCallResponse = {
choices: [{
message: {
role: 'assistant' as const,
tool_calls: [{
id: 'call_1',
type: 'function' as const,
function: { name: 'github_list_issues', arguments: '{}' }
}]
choices: [
{
message: {
role: 'assistant' as const,
tool_calls: [
{
id: 'call_1',
type: 'function' as const,
function: { name: 'github_list_issues', arguments: '{}' },
},
],
},
finish_reason: 'tool_calls' as const,
},
finish_reason: 'tool_calls' as const
}]
],
};
// Mock to always return tool calls (infinite loop scenario)
@@ -325,17 +327,16 @@ describe('AgentLoopService', () => {
startTime: Date.now() - 100,
endTime: Date.now(),
executionTimeMs: 100,
result: '{}'
result: '{}',
};
const mockRegistryInstance = mockToolRegistry.getInstance();
mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
const result = await service.executeTask(
'Infinite loop test',
'conv_123',
{ maxIterations: 2, timeoutMs: 10000 }
);
const result = await service.executeTask('Infinite loop test', 'conv_123', {
maxIterations: 2,
timeoutMs: 10000,
});
expect(result.status).toBe('max_iterations');
expect(result.iterations).toBe(2);
@@ -344,31 +345,34 @@ describe('AgentLoopService', () => {
test('should handle tool execution error gracefully', async () => {
const mockToolCallResponse = {
choices: [{
message: {
role: 'assistant' as const,
tool_calls: [{
id: 'call_1',
type: 'function' as const,
function: {
name: 'invalid_tool',
arguments: '{}'
}
}]
choices: [
{
message: {
role: 'assistant' as const,
tool_calls: [
{
id: 'call_1',
type: 'function' as const,
function: { name: 'invalid_tool', arguments: '{}' },
},
],
},
finish_reason: 'tool_calls' as const,
},
finish_reason: 'tool_calls' as const
}]
],
};
const mockErrorResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: 'I encountered an error while executing the tool.',
tool_calls: undefined
choices: [
{
message: {
role: 'assistant' as const,
content: 'I encountered an error while executing the tool.',
tool_calls: undefined,
},
finish_reason: 'stop' as const,
},
finish_reason: 'stop' as const
}]
],
};
const mockToolExecution: AgentToolExecution = {
@@ -380,7 +384,7 @@ describe('AgentLoopService', () => {
startTime: Date.now() - 100,
endTime: Date.now(),
executionTimeMs: 100,
errorMessage: 'Tool not found'
errorMessage: 'Tool not found',
};
const mockRegistryInstance = mockToolRegistry.getInstance();
@@ -390,10 +394,7 @@ describe('AgentLoopService', () => {
.mockResolvedValueOnce({ data: mockToolCallResponse })
.mockResolvedValueOnce({ data: mockErrorResponse });
const result = await service.executeTask(
'Test error handling',
'conv_123'
);
const result = await service.executeTask('Test error handling', 'conv_123');
expect(result.status).toBe('completed');
expect(result.toolExecutions).toHaveLength(1);
@@ -404,10 +405,7 @@ describe('AgentLoopService', () => {
test('should handle API client errors', async () => {
mockApiClient.post.mockRejectedValue(new Error('Network error'));
const result = await service.executeTask(
'Test API error',
'conv_123'
);
const result = await service.executeTask('Test API error', 'conv_123');
expect(result.status).toBe('error');
expect(result.error).toContain('Network error');
@@ -417,31 +415,33 @@ describe('AgentLoopService', () => {
test('should parse tool name from function name correctly', async () => {
const mockToolCallResponse = {
choices: [{
message: {
role: 'assistant' as const,
tool_calls: [{
id: 'call_1',
type: 'function' as const,
function: {
name: 'github_list_issues', // Should parse to skillId=github, toolName=list_issues
arguments: '{"owner":"user","repo":"test"}'
}
}]
choices: [
{
message: {
role: 'assistant' as const,
tool_calls: [
{
id: 'call_1',
type: 'function' as const,
function: {
name: 'github_list_issues', // Should parse to skillId=github, toolName=list_issues
arguments: '{"owner":"user","repo":"test"}',
},
},
],
},
finish_reason: 'tool_calls' as const,
},
finish_reason: 'tool_calls' as const
}]
],
};
const mockFinalResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: 'Done',
tool_calls: undefined
choices: [
{
message: { role: 'assistant' as const, content: 'Done', tool_calls: undefined },
finish_reason: 'stop' as const,
},
finish_reason: 'stop' as const
}]
],
};
const mockToolExecution: AgentToolExecution = {
@@ -453,7 +453,7 @@ describe('AgentLoopService', () => {
startTime: Date.now() - 100,
endTime: Date.now(),
executionTimeMs: 100,
result: '{}'
result: '{}',
};
const mockRegistryInstance = mockToolRegistry.getInstance();
@@ -486,13 +486,12 @@ describe('AgentLoopService', () => {
describe('task execution options', () => {
test('should use default options when none provided', async () => {
const mockResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: 'Test response'
choices: [
{
message: { role: 'assistant' as const, content: 'Test response' },
finish_reason: 'stop' as const,
},
finish_reason: 'stop' as const
}]
],
};
mockApiClient.post.mockResolvedValue({ data: mockResponse });
@@ -506,13 +505,12 @@ describe('AgentLoopService', () => {
test('should respect custom execution options', async () => {
const mockResponse = {
choices: [{
message: {
role: 'assistant' as const,
content: 'Test response'
choices: [
{
message: { role: 'assistant' as const, content: 'Test response' },
finish_reason: 'stop' as const,
},
finish_reason: 'stop' as const
}]
],
};
mockApiClient.post.mockResolvedValue({ data: mockResponse });
@@ -521,7 +519,7 @@ describe('AgentLoopService', () => {
maxIterations: 3,
timeoutMs: 5000,
model: 'gpt-3.5-turbo',
temperature: 0.7
temperature: 0.7,
};
const result = await service.executeTask('Test', 'conv_123', customOptions);
@@ -531,11 +529,8 @@ describe('AgentLoopService', () => {
// Verify custom options were passed to API
expect(mockApiClient.post).toHaveBeenCalledWith(
'/api/v1/conversations/conv_123/messages',
expect.objectContaining({
model: 'gpt-3.5-turbo',
temperature: 0.7
})
expect.objectContaining({ model: 'gpt-3.5-turbo', temperature: 0.7 })
);
});
});
});
});
@@ -1,6 +1,7 @@
import { describe, test, expect, beforeEach, vi, type Mock } from 'vitest';
import { AgentToolRegistry } from '../agentToolRegistry';
import { invoke } from '@tauri-apps/api/core';
import { beforeEach, describe, expect, type Mock, test, vi } from 'vitest';
import { AgentToolRegistry } from '../agentToolRegistry';
// Mock Tauri invoke
vi.mock('@tauri-apps/api/core');
@@ -19,35 +20,35 @@ describe('AgentToolRegistry', () => {
test('should load tool schemas from Tauri using ZeroClaw format', async () => {
const mockSchemas = [
{
type: "function",
type: 'function',
function: {
name: "github_list_issues",
description: "List GitHub issues for a repository",
name: 'github_list_issues',
description: 'List GitHub issues for a repository',
parameters: {
type: "object",
type: 'object',
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" }
owner: { type: 'string', description: 'Repository owner' },
repo: { type: 'string', description: 'Repository name' },
},
required: ["owner", "repo"]
}
}
required: ['owner', 'repo'],
},
},
},
{
type: "function",
type: 'function',
function: {
name: "notion_create_page",
description: "Create a new Notion page",
name: 'notion_create_page',
description: 'Create a new Notion page',
parameters: {
type: "object",
type: 'object',
properties: {
title: { type: "string", description: "Page title" },
content: { type: "string", description: "Page content" }
title: { type: 'string', description: 'Page title' },
content: { type: 'string', description: 'Page content' },
},
required: ["title"]
}
}
}
required: ['title'],
},
},
},
];
mockInvoke.mockResolvedValue(mockSchemas);
@@ -55,21 +56,21 @@ describe('AgentToolRegistry', () => {
const schemas = await service.loadToolSchemas();
expect(schemas).toHaveLength(2);
expect(schemas[0].function.name).toBe("github_list_issues");
expect(schemas[1].function.name).toBe("notion_create_page");
expect(schemas[0].function.name).toBe('github_list_issues');
expect(schemas[1].function.name).toBe('notion_create_page');
expect(mockInvoke).toHaveBeenCalledWith('runtime_get_tool_schemas');
});
test('should cache tool schemas to avoid repeated calls', async () => {
const mockSchemas = [
{
type: "function",
type: 'function',
function: {
name: "test_tool",
description: "Test tool",
parameters: { type: "object", properties: {} }
}
}
name: 'test_tool',
description: 'Test tool',
parameters: { type: 'object', properties: {} },
},
},
];
mockInvoke.mockResolvedValue(mockSchemas);
@@ -87,13 +88,13 @@ describe('AgentToolRegistry', () => {
test('should force reload when requested', async () => {
const mockSchemas = [
{
type: "function",
type: 'function',
function: {
name: "test_tool",
description: "Test tool",
parameters: { type: "object", properties: {} }
}
}
name: 'test_tool',
description: 'Test tool',
parameters: { type: 'object', properties: {} },
},
},
];
mockInvoke.mockResolvedValue(mockSchemas);
@@ -120,7 +121,9 @@ describe('AgentToolRegistry', () => {
const errorMessage = 'Failed to load tool schemas';
mockInvoke.mockRejectedValue(new Error(errorMessage));
await expect(service.loadToolSchemas()).rejects.toThrow(`Failed to load tool schemas: Error: ${errorMessage}`);
await expect(service.loadToolSchemas()).rejects.toThrow(
`Failed to load tool schemas: Error: ${errorMessage}`
);
});
});
@@ -130,7 +133,7 @@ describe('AgentToolRegistry', () => {
success: true,
output: '{"issues": [{"title": "Bug fix", "number": 1}]}',
error: null,
execution_time: 1500
execution_time: 1500,
};
mockInvoke.mockResolvedValue(mockResult);
@@ -150,7 +153,7 @@ describe('AgentToolRegistry', () => {
// Verify correct tool_id format and arguments
expect(mockInvoke).toHaveBeenCalledWith('runtime_execute_tool', {
toolId: 'github_list_issues',
arguments: '{"owner":"user","repo":"test"}'
arguments: '{"owner":"user","repo":"test"}',
});
});
@@ -159,16 +162,12 @@ describe('AgentToolRegistry', () => {
success: false,
output: '',
error: 'Tool not found: invalid_tool',
execution_time: 100
execution_time: 100,
};
mockInvoke.mockResolvedValue(mockResult);
const result = await service.executeTool(
'invalid',
'tool',
'{}'
);
const result = await service.executeTool('invalid', 'tool', '{}');
expect(result.status).toBe('error');
expect(result.errorMessage).toBe('Tool not found: invalid_tool');
@@ -180,7 +179,7 @@ describe('AgentToolRegistry', () => {
const mockResult = {
success: true,
output: 'Success',
error: null
error: null,
// No execution_time provided
};
@@ -208,12 +207,7 @@ describe('AgentToolRegistry', () => {
});
test('should generate unique execution IDs', async () => {
const mockResult = {
success: true,
output: 'test',
error: null,
execution_time: 100
};
const mockResult = { success: true, output: 'test', error: null, execution_time: 100 };
mockInvoke.mockResolvedValue(mockResult);
@@ -230,29 +224,29 @@ describe('AgentToolRegistry', () => {
beforeEach(async () => {
const mockSchemas = [
{
type: "function",
type: 'function',
function: {
name: "github_list_issues",
description: "List GitHub issues",
parameters: { type: "object", properties: {} }
}
name: 'github_list_issues',
description: 'List GitHub issues',
parameters: { type: 'object', properties: {} },
},
},
{
type: "function",
type: 'function',
function: {
name: "github_create_issue",
description: "Create GitHub issue",
parameters: { type: "object", properties: {} }
}
name: 'github_create_issue',
description: 'Create GitHub issue',
parameters: { type: 'object', properties: {} },
},
},
{
type: "function",
type: 'function',
function: {
name: "notion_create_page",
description: "Create Notion page",
parameters: { type: "object", properties: {} }
}
}
name: 'notion_create_page',
description: 'Create Notion page',
parameters: { type: 'object', properties: {} },
},
},
];
mockInvoke.mockResolvedValue(mockSchemas);
@@ -280,7 +274,7 @@ describe('AgentToolRegistry', () => {
expect(tools.map(t => t.function.name)).toEqual([
'github_list_issues',
'github_create_issue',
'notion_create_page'
'notion_create_page',
]);
});
@@ -294,7 +288,7 @@ describe('AgentToolRegistry', () => {
expect(toolsBySkill.github.map(t => t.function.name)).toEqual([
'github_list_issues',
'github_create_issue'
'github_create_issue',
]);
expect(toolsBySkill.notion[0].function.name).toBe('notion_create_page');
});
@@ -341,13 +335,13 @@ describe('AgentToolRegistry', () => {
test('should clear cached tool schemas', async () => {
const mockSchemas = [
{
type: "function",
type: 'function',
function: {
name: "test_tool",
description: "Test tool",
parameters: { type: "object", properties: {} }
}
}
name: 'test_tool',
description: 'Test tool',
parameters: { type: 'object', properties: {} },
},
},
];
mockInvoke.mockResolvedValue(mockSchemas);
@@ -364,4 +358,4 @@ describe('AgentToolRegistry', () => {
expect(mockInvoke).toHaveBeenCalledTimes(2);
});
});
});
});
+14 -31
View File
@@ -6,22 +6,14 @@
* - runtime_get_tool_schemas: Get all tools in OpenAI-compatible format
* - runtime_execute_tool: Execute a tool with enhanced validation and timing
*/
import { invoke } from '@tauri-apps/api/core';
import type {
AgentToolSchema,
AgentToolExecution,
IAgentToolRegistry
} from '../types/agent';
import type { AgentToolExecution, AgentToolSchema, IAgentToolRegistry } from '../types/agent';
// ZeroClaw format types from Rust
interface ZeroClawToolSchema {
type: string;
function: {
name: string;
description: string;
parameters: any;
};
function: { name: string; description: string; parameters: any };
}
interface ZeroClawToolResult {
@@ -51,7 +43,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
const now = Date.now();
// Return cached tools if still fresh
if (!forceReload && this.toolSchemas.length > 0 && (now - this.lastLoadTime) < this.CACHE_TTL) {
if (!forceReload && this.toolSchemas.length > 0 && now - this.lastLoadTime < this.CACHE_TTL) {
return this.toolSchemas;
}
@@ -69,8 +61,8 @@ export class AgentToolRegistry implements IAgentToolRegistry {
function: {
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters
}
parameters: tool.function.parameters,
},
}));
this.lastLoadTime = now;
@@ -110,7 +102,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
} catch (e) {
return 'Failed to parse: ' + e;
}
})()
})(),
});
const execution: AgentToolExecution = {
@@ -119,7 +111,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
skillId,
arguments: toolArguments,
status: 'running',
startTime
startTime,
};
try {
@@ -130,15 +122,15 @@ export class AgentToolRegistry implements IAgentToolRegistry {
console.log(` args type: ${typeof toolArguments}`);
const result = await invoke<ZeroClawToolResult>('runtime_execute_tool', {
toolId: toolId, // Use camelCase as expected by current Rust version
args: toolArguments // Use "args" instead of "arguments"
toolId: toolId, // Use camelCase as expected by current Rust version
args: toolArguments, // Use "args" instead of "arguments"
});
console.log(`🔧 [AFTER INVOKE] Tool execution result:`, result);
execution.endTime = Date.now();
// Use execution time from Rust if available, otherwise calculate locally
execution.executionTimeMs = result.execution_time || (execution.endTime - execution.startTime);
execution.executionTimeMs = result.execution_time || execution.endTime - execution.startTime;
if (!result.success) {
execution.status = 'error';
@@ -155,7 +147,6 @@ export class AgentToolRegistry implements IAgentToolRegistry {
}
return execution;
} catch (error) {
execution.endTime = Date.now();
execution.executionTimeMs = execution.endTime - execution.startTime;
@@ -205,11 +196,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
/**
* Get tool execution statistics
*/
getToolStats(): {
totalTools: number;
skillCount: number;
categories: Record<string, number>;
} {
getToolStats(): { totalTools: number; skillCount: number; categories: Record<string, number> } {
const categories: Record<string, number> = {};
const skills = new Set<string>();
@@ -222,11 +209,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
categories[category] = (categories[category] || 0) + 1;
}
return {
totalTools: this.toolSchemas.length,
skillCount: skills.size,
categories
};
return { totalTools: this.toolSchemas.length, skillCount: skills.size, categories };
}
/**
@@ -272,4 +255,4 @@ export class AgentToolRegistry implements IAgentToolRegistry {
return 'Other';
}
}
}
+4 -2
View File
@@ -32,7 +32,9 @@ export const actionableItemsApi = {
* GET /telegram/actionable-items
*/
getActionableItems: async (): Promise<ActionableItem[]> => {
const response = await apiClient.get<ApiResponse<ActionableItem[]>>('/telegram/actionable-items');
const response = await apiClient.get<ApiResponse<ActionableItem[]>>(
'/telegram/actionable-items'
);
return response.data;
},
@@ -110,4 +112,4 @@ export const actionableItemsApi = {
);
return response.data;
},
};
};
+1 -1
View File
@@ -50,4 +50,4 @@ export const apiKeysApi = {
revokeApiKey: async (keyId: string): Promise<void> => {
await apiClient.delete<ApiResponse<unknown>>(`/api-keys/${keyId}`);
},
};
};
+2 -5
View File
@@ -39,13 +39,10 @@ export const creditsApi = {
* Get paginated credit transaction history
* GET /credits/transactions
*/
getTransactions: async (
page = 1,
limit = 50
): Promise<PaginatedTransactions> => {
getTransactions: async (page = 1, limit = 50): Promise<PaginatedTransactions> => {
const response = await apiClient.get<ApiResponse<PaginatedTransactions>>(
`/credits/transactions?page=${page}&limit=${limit}`
);
return response.data;
},
};
};
+1 -1
View File
@@ -65,4 +65,4 @@ export const feedbackApi = {
const response = await apiClient.put<ApiResponse<FeedbackItem>>(`/feedback/${id}`, updates);
return response.data;
},
};
};
+1 -1
View File
@@ -35,4 +35,4 @@ export const settingsApi = {
setPlatformsConnected: async (platforms: string[]): Promise<void> => {
await apiClient.post<ApiResponse<unknown>>('/settings/platforms-connected', { platforms });
},
};
};
+4 -7
View File
@@ -1,3 +1,5 @@
import { injectAll } from '../../lib/ai/injector';
import type { Message } from '../../lib/ai/providers/interface';
import type { ApiResponse } from '../../types/api';
import type {
PurgeRequestBody,
@@ -10,8 +12,6 @@ import type {
ThreadsListData,
} from '../../types/thread';
import { apiClient } from '../apiClient';
import { injectAll } from '../../lib/ai/injector';
import type { Message } from '../../lib/ai/providers/interface';
export const threadApi = {
/** GET /threads — list all threads for the authenticated user */
@@ -55,14 +55,11 @@ export const threadApi = {
if (options.injectSoul) {
try {
const userMessage: Message = {
role: 'user',
content: [{ type: 'text', text: message }]
};
const userMessage: Message = { role: 'user', content: [{ type: 'text', text: message }] };
const injectedMessage = await injectAll(userMessage, {
mode: 'context-block',
includeMetadata: false
includeMetadata: false,
});
// Extract the processed text
+6 -21
View File
@@ -92,10 +92,7 @@ const daemonSlice = createSlice({
}
},
setDaemonStatus: (
state,
action: PayloadAction<{ userId: string; status: DaemonStatus }>
) => {
setDaemonStatus: (state, action: PayloadAction<{ userId: string; status: DaemonStatus }>) => {
const { userId, status } = action.payload;
const user = ensureUserState(state, userId);
user.status = status;
@@ -107,37 +104,25 @@ const daemonSlice = createSlice({
}
},
incrementConnectionAttempts: (
state,
action: PayloadAction<{ userId: string }>
) => {
incrementConnectionAttempts: (state, action: PayloadAction<{ userId: string }>) => {
const { userId } = action.payload;
const user = ensureUserState(state, userId);
user.connectionAttempts += 1;
},
resetConnectionAttempts: (
state,
action: PayloadAction<{ userId: string }>
) => {
resetConnectionAttempts: (state, action: PayloadAction<{ userId: string }>) => {
const { userId } = action.payload;
const user = ensureUserState(state, userId);
user.connectionAttempts = 0;
},
setAutoStartEnabled: (
state,
action: PayloadAction<{ userId: string; enabled: boolean }>
) => {
setAutoStartEnabled: (state, action: PayloadAction<{ userId: string; enabled: boolean }>) => {
const { userId, enabled } = action.payload;
const user = ensureUserState(state, userId);
user.autoStartEnabled = enabled;
},
setIsRecovering: (
state,
action: PayloadAction<{ userId: string; isRecovering: boolean }>
) => {
setIsRecovering: (state, action: PayloadAction<{ userId: string; isRecovering: boolean }>) => {
const { userId, isRecovering } = action.payload;
const user = ensureUserState(state, userId);
user.isRecovering = isRecovering;
@@ -211,4 +196,4 @@ export const selectIsDaemonRecovering = (state: { daemon: DaemonState }, userId?
return daemonState.isRecovering;
};
export default daemonSlice.reducer;
export default daemonSlice.reducer;
-1
View File
@@ -53,7 +53,6 @@ const threadPersistConfig = {
whitelist: ['panelWidth', 'lastViewedAt', 'threads', 'messagesByThreadId', 'selectedThreadId'],
};
const persistedAuthReducer = persistReducer(authPersistConfig, authReducer);
const persistedAiReducer = persistReducer(aiPersistConfig, aiReducer);
const persistedSkillsReducer = persistReducer(skillsPersistConfig, skillsReducer);
+8 -8
View File
@@ -1,9 +1,9 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { threadApi } from '../services/api/threadApi';
import type { Thread, ThreadMessage } from '../types/thread';
import { injectAll } from '../lib/ai/injector';
import type { Message } from '../lib/ai/providers/interface';
import { threadApi } from '../services/api/threadApi';
import type { Thread, ThreadMessage } from '../types/thread';
import type { RootState } from './index';
interface ThreadState {
@@ -101,14 +101,11 @@ export const sendMessage = createAsyncThunk(
// 2. Process message with SOUL + TOOLS injection before sending to API
let processedMessage = message;
try {
const userMessage: Message = {
role: 'user',
content: [{ type: 'text', text: message }]
};
const userMessage: Message = { role: 'user', content: [{ type: 'text', text: message }] };
const injectedMessage = await injectAll(userMessage, {
mode: 'context-block',
includeMetadata: false
includeMetadata: false,
});
// Extract the processed text
@@ -119,7 +116,10 @@ export const sendMessage = createAsyncThunk(
console.log('✅ SOUL + TOOLS injection successful in Redux sendMessage thunk');
} catch (injectionError) {
console.warn('⚠️ SOUL + TOOLS injection failed in Redux sendMessage thunk:', injectionError);
console.warn(
'⚠️ SOUL + TOOLS injection failed in Redux sendMessage thunk:',
injectionError
);
// Continue with original message
}
+2 -4
View File
@@ -2,10 +2,8 @@
declare global {
interface Window {
__TAURI__?: {
[key: string]: unknown;
};
__TAURI__?: { [key: string]: unknown };
}
}
export {};
export {};
+1 -1
View File
@@ -23,4 +23,4 @@ export interface OAuthError {
provider: OAuthProvider;
message: string;
code?: string;
}
}
+1 -1
View File
@@ -1,12 +1,12 @@
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { skillManager } from '../lib/skills/manager';
import { consumeLoginToken, fetchIntegrationTokens } from '../services/api/authApi';
import { buildManualSentryEvent, enqueueError } from '../services/errorReportQueue';
import { store } from '../store';
import { setToken } from '../store/authSlice';
import { setSkillState } from '../store/skillsSlice';
import { skillManager } from '../lib/skills/manager';
import {
decryptIntegrationTokens,
hexToBase64,
+3 -5
View File
@@ -4,6 +4,7 @@
* Helper functions for invoking Tauri commands from the frontend.
*/
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
import { injectAll } from '../lib/ai/injector';
import type { Message } from '../lib/ai/providers/interface';
@@ -405,14 +406,11 @@ export async function alphahumanAgentChat(
if (options.injectSoul) {
try {
const userMessage: Message = {
role: 'user',
content: [{ type: 'text', text: message }]
};
const userMessage: Message = { role: 'user', content: [{ type: 'text', text: message }] };
const injectedMessage = await injectAll(userMessage, {
mode: 'context-block',
includeMetadata: false
includeMetadata: false,
});
// Extract the processed text