mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 00:06:42 +02:00
Add agent knowledge files for all modules
This commit is contained in:
parent
11324b5e68
commit
dd06bb2e06
243 changed files with 50805 additions and 175 deletions
25
tools/agent-knowledge/.gitignore
vendored
Normal file
25
tools/agent-knowledge/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Test outputs
|
||||
test-project/
|
||||
112
tools/agent-knowledge/.knowledge/claude-scan-prompt.md
Normal file
112
tools/agent-knowledge/.knowledge/claude-scan-prompt.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# Agent Knowledge System - Project Analysis Request
|
||||
|
||||
## Your Task
|
||||
|
||||
Analyze this codebase and help set up the Agent Knowledge System with appropriate agents for each module/package.
|
||||
|
||||
## What to Analyze
|
||||
|
||||
1. **Project Structure**: Scan the directory structure to understand the monorepo layout
|
||||
2. **Package Types**: Identify apps, packages, services, libraries, and their purposes
|
||||
3. **Dependencies**: Look at package.json files to understand relationships
|
||||
4. **Tech Stack**: Identify frameworks (NestJS, SvelteKit, Expo, Astro, etc.)
|
||||
5. **Shared Code**: Find shared packages that are used across multiple apps
|
||||
|
||||
## Analysis Steps
|
||||
|
||||
### Step 1: Discover Structure
|
||||
|
||||
Run these commands to understand the project:
|
||||
|
||||
```bash
|
||||
# List top-level directories
|
||||
ls -la
|
||||
|
||||
# Find all package.json files
|
||||
find . -name "package.json" -not -path "*/node_modules/*" | head -50
|
||||
|
||||
# Find common config files to identify tech stack
|
||||
find . -name "nest-cli.json" -o -name "svelte.config.js" -o -name "app.json" -o -name "astro.config.mjs" 2>/dev/null | head -20
|
||||
```
|
||||
|
||||
### Step 2: Identify High-Value Modules
|
||||
|
||||
Look for modules that would benefit most from dedicated agents:
|
||||
|
||||
- **Auth modules** - Critical, used everywhere
|
||||
- **Core/shared packages** - Foundation code
|
||||
- **API clients** - Integration points
|
||||
- **UI components** - Design system
|
||||
- **Database packages** - Data layer
|
||||
|
||||
### Step 3: Recommend Agent Setup
|
||||
|
||||
For each important module, suggest:
|
||||
|
||||
1. Whether it needs a **single agent** or a **team**
|
||||
2. What **watch patterns** should track changes to this module
|
||||
3. What **related modules** the agent should also know about
|
||||
|
||||
## Output Format
|
||||
|
||||
Please provide your analysis in this format:
|
||||
|
||||
### Project Overview
|
||||
|
||||
- Type: [monorepo/single-app/etc]
|
||||
- Primary tech stack: [list technologies]
|
||||
- Total packages/apps: [count]
|
||||
|
||||
### Recommended Agents
|
||||
|
||||
#### High Priority (Create First)
|
||||
|
||||
| Path | Agent Type | Reason |
|
||||
| -------------------- | ------------ | ---------------------------------- |
|
||||
| packages/shared-auth | Single Agent | Auth is critical, used by all apps |
|
||||
| ... | ... | ... |
|
||||
|
||||
#### Medium Priority
|
||||
|
||||
| Path | Agent Type | Reason |
|
||||
| ---- | ---------- | ------ |
|
||||
| ... | ... | ... |
|
||||
|
||||
### Recommended Teams
|
||||
|
||||
For complex apps that need full team coverage:
|
||||
| Path | Template | Reason |
|
||||
|------|----------|--------|
|
||||
| apps/main-app | standard | Complex app with many features |
|
||||
| ... | ... | ... |
|
||||
|
||||
### Watch Pattern Suggestions
|
||||
|
||||
```yaml
|
||||
# Example for auth module
|
||||
packages/shared-auth:
|
||||
watches:
|
||||
- packages/shared-auth/**
|
||||
- packages/shared-auth-*/** # Related auth packages
|
||||
- apps/*/src/**/auth/** # Auth code in apps
|
||||
- services/auth-service/** # Auth service
|
||||
```
|
||||
|
||||
## After Analysis
|
||||
|
||||
Once you provide the analysis, I can run these commands to set up the agents:
|
||||
|
||||
```bash
|
||||
# Initialize the system (if not done)
|
||||
npx agent-knowledge init
|
||||
|
||||
# Add recommended agents
|
||||
npx agent-knowledge add-agent <path>
|
||||
|
||||
# Add teams to complex apps
|
||||
npx agent-knowledge team <path> --template <template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Please start by exploring the project structure and providing your analysis.**
|
||||
17
tools/agent-knowledge/.npmignore
Normal file
17
tools/agent-knowledge/.npmignore
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Development files
|
||||
node_modules/
|
||||
.git/
|
||||
.github/
|
||||
test/
|
||||
coverage/
|
||||
*.log
|
||||
|
||||
# Editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
419
tools/agent-knowledge/README.md
Normal file
419
tools/agent-knowledge/README.md
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
# Agent Knowledge System
|
||||
|
||||
AI agents that live in your codebase and automatically learn from your code changes. Perfect for monorepos where you want specialized agents for each module that stay up-to-date with the code.
|
||||
|
||||
## Features
|
||||
|
||||
- **Co-located Agents** - Agents live in `.agent/` folders alongside the code they know
|
||||
- **Auto-updating Knowledge** - Git hooks track changes, nightly jobs update agent memory
|
||||
- **Multi-provider Support** - Works with OpenRouter (many models) or Anthropic directly
|
||||
- **Claude Code Integration** - Agents work seamlessly with Claude Code
|
||||
- **Monorepo Ready** - Perfect for large codebases with multiple packages/modules
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Initialize in your project
|
||||
npx agent-knowledge init
|
||||
|
||||
# Add an agent to a module
|
||||
npx agent-knowledge add-agent packages/shared-auth
|
||||
|
||||
# Check status
|
||||
npx agent-knowledge status
|
||||
|
||||
# Manually update agent knowledge
|
||||
npx agent-knowledge update
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Your Monorepo │
|
||||
│ │
|
||||
│ packages/shared-auth/ │
|
||||
│ ├── src/ │
|
||||
│ ├── .agent/ ◄── Agent lives with the code │
|
||||
│ │ ├── agent.md Persona & instructions │
|
||||
│ │ ├── memory.md Auto-updated knowledge │
|
||||
│ │ └── architecture.md Manual architecture docs │
|
||||
│ └── package.json │
|
||||
│ │
|
||||
│ .knowledge/ ◄── System configuration │
|
||||
│ ├── config.yaml Provider settings │
|
||||
│ ├── agent-registry.yaml Tracks all agents │
|
||||
│ ├── changes.jsonl Pending changes log │
|
||||
│ └── .env API keys (gitignored) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
│ Git Commit │ Nightly Cron
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────────┐
|
||||
│ Track changes │ │ LLM summarizes │
|
||||
│ in changes.jsonl│ │ updates memory.md │
|
||||
└─────────────────┘ └─────────────────────┘
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Global Install (optional)
|
||||
|
||||
```bash
|
||||
npm install -g agent-knowledge
|
||||
```
|
||||
|
||||
### Per-project (recommended)
|
||||
|
||||
Just use `npx`:
|
||||
|
||||
```bash
|
||||
npx agent-knowledge init
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `scan`
|
||||
|
||||
Auto-discover your project structure and suggest agents.
|
||||
|
||||
```bash
|
||||
npx agent-knowledge scan
|
||||
|
||||
# Options
|
||||
--depth <n> # Max directory depth (default: 4)
|
||||
--all # Show all modules (not just top 5 per type)
|
||||
--json # Output as JSON for scripting
|
||||
--dry-run # Preview without creating
|
||||
```
|
||||
|
||||
**Example output for ManaCore-like monorepo:**
|
||||
```
|
||||
📁 Discovered Structure:
|
||||
|
||||
📦 package (42)
|
||||
⭐ packages/shared-auth
|
||||
⭐ packages/shared-api-client
|
||||
○ packages/shared-ui
|
||||
...
|
||||
|
||||
📱 app (20)
|
||||
○ apps/nutriphi
|
||||
○ apps/manacore
|
||||
...
|
||||
|
||||
⚙️ service (1)
|
||||
⭐ services/mana-core-auth
|
||||
|
||||
🎯 Recommended Agents (high priority):
|
||||
packages/shared-auth
|
||||
Type: package | Reason: Contains "auth"
|
||||
```
|
||||
|
||||
### `team`
|
||||
|
||||
Add a full development team to a project.
|
||||
|
||||
```bash
|
||||
# List available team templates
|
||||
npx agent-knowledge team --list
|
||||
|
||||
# Add standard team (6 agents)
|
||||
npx agent-knowledge team apps/nutriphi
|
||||
|
||||
# Add startup team (3 agents) - lean
|
||||
npx agent-knowledge team packages/shared-auth --template startup
|
||||
|
||||
# Add enterprise team (10 agents) - full coverage
|
||||
npx agent-knowledge team apps/manacore --template enterprise
|
||||
|
||||
# Preview what would be created
|
||||
npx agent-knowledge team apps/myapp --dry-run
|
||||
```
|
||||
|
||||
**Team Templates:**
|
||||
|
||||
| Template | Agents | Roles |
|
||||
|----------|--------|-------|
|
||||
| `startup` | 3 | Tech Lead, Developer, QA |
|
||||
| `standard` | 6 | Product Owner, Architect, Senior Dev, Dev, Security, QA Lead |
|
||||
| `enterprise` | 10 | + Scrum Master, DevOps, Frontend/Backend Specialists, Tech Writer |
|
||||
|
||||
### `init`
|
||||
|
||||
Initialize the agent knowledge system in your project.
|
||||
|
||||
```bash
|
||||
npx agent-knowledge init
|
||||
|
||||
# Options
|
||||
--provider <provider> # LLM provider: openrouter (default) | anthropic
|
||||
--model <model> # Default model for updates
|
||||
--no-hooks # Skip git hooks installation
|
||||
```
|
||||
|
||||
### `add-agent <path>`
|
||||
|
||||
Add an agent to a module/package.
|
||||
|
||||
```bash
|
||||
npx agent-knowledge add-agent packages/shared-auth
|
||||
npx agent-knowledge add-agent backends/api-gateway
|
||||
|
||||
# Options
|
||||
--name <name> # Custom agent name
|
||||
--watches <patterns> # Additional watch patterns
|
||||
```
|
||||
|
||||
### `update`
|
||||
|
||||
Update agent knowledge from pending changes.
|
||||
|
||||
```bash
|
||||
npx agent-knowledge update
|
||||
|
||||
# Options
|
||||
--agent <path> # Update specific agent only
|
||||
--model <model> # Override model for this run
|
||||
--dry-run # Preview without making changes
|
||||
```
|
||||
|
||||
### `status`
|
||||
|
||||
Show system status and all registered agents.
|
||||
|
||||
```bash
|
||||
npx agent-knowledge status
|
||||
```
|
||||
|
||||
### `setup-hooks`
|
||||
|
||||
Install or reinstall git hooks.
|
||||
|
||||
```bash
|
||||
npx agent-knowledge setup-hooks
|
||||
--force # Overwrite existing hooks
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### LLM Providers
|
||||
|
||||
The system supports two providers:
|
||||
|
||||
**OpenRouter (recommended)** - Access to many models through one API:
|
||||
|
||||
```yaml
|
||||
# .knowledge/config.yaml
|
||||
provider: openrouter
|
||||
model: google/gemini-flash-1.5 # Cheapest option
|
||||
```
|
||||
|
||||
**Anthropic** - Direct Claude API:
|
||||
|
||||
```yaml
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
### Available Models (OpenRouter)
|
||||
|
||||
| Model | Cost | Speed | Quality | Best For |
|
||||
|-------|------|-------|---------|----------|
|
||||
| `google/gemini-flash-1.5` | $0.075/1M | Fast | Good | Daily batch jobs |
|
||||
| `deepseek/deepseek-chat` | $0.14/1M | Fast | Good | Budget option |
|
||||
| `anthropic/claude-3-haiku` | $0.25/1M | Fast | Good | Reliable default |
|
||||
| `anthropic/claude-sonnet-4-20250514` | $3/1M | Medium | Excellent | Important updates |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set in `.knowledge/.env` (gitignored) or as environment variables:
|
||||
|
||||
```bash
|
||||
# OpenRouter
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxx
|
||||
LLM_PROVIDER=openrouter
|
||||
OPENROUTER_MODEL=google/gemini-flash-1.5
|
||||
|
||||
# Anthropic (alternative)
|
||||
ANTHROPIC_API_KEY=sk-ant-xxxxx
|
||||
LLM_PROVIDER=anthropic
|
||||
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
## Automated Updates
|
||||
|
||||
### Cron Job (local)
|
||||
|
||||
```bash
|
||||
# Add to crontab: crontab -e
|
||||
# Run at 3 AM every night
|
||||
0 3 * * * cd /path/to/project && npx agent-knowledge update >> .knowledge/cron.log 2>&1
|
||||
```
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
# .github/workflows/update-agent-knowledge.yml
|
||||
name: Update Agent Knowledge
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # 3 AM UTC daily
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Update agent knowledge
|
||||
run: npx agent-knowledge update
|
||||
env:
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
|
||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "🤖 Update agent knowledge"
|
||||
file_pattern: "**/.agent/memory.md .knowledge/archive/*"
|
||||
```
|
||||
|
||||
## Using with Claude Code
|
||||
|
||||
Once set up, use agents naturally with Claude Code:
|
||||
|
||||
```
|
||||
# When working on auth module
|
||||
> Read packages/shared-auth/.agent/ and help me implement refresh tokens
|
||||
|
||||
# Claude will:
|
||||
# 1. Read agent.md (persona, principles)
|
||||
# 2. Read memory.md (recent changes, context)
|
||||
# 3. Read architecture.md (structure)
|
||||
# 4. Help with full context of the module
|
||||
```
|
||||
|
||||
### CLAUDE.md Integration
|
||||
|
||||
The init command automatically adds agent system documentation to your `CLAUDE.md`:
|
||||
|
||||
```markdown
|
||||
## Agent Knowledge System
|
||||
|
||||
When working on a specific module, check for `.agent/agent.md` in that directory.
|
||||
If found, read all files in `.agent/` to load domain knowledge and adopt that
|
||||
agent's persona and expertise.
|
||||
```
|
||||
|
||||
## Agent File Structure
|
||||
|
||||
Each `.agent/` folder contains:
|
||||
|
||||
```
|
||||
.agent/
|
||||
├── agent.md # Persona, role, expertise, principles
|
||||
├── memory.md # Auto-updated from code changes
|
||||
├── architecture.md # Manual architecture documentation
|
||||
└── [custom].md # Any additional context files
|
||||
```
|
||||
|
||||
### agent.md (example)
|
||||
|
||||
```markdown
|
||||
# Auth Module Expert
|
||||
|
||||
## Identity
|
||||
You are the **Auth Module Expert** for this codebase. Deep knowledge of
|
||||
authentication flows, OAuth, JWT handling, and session management.
|
||||
|
||||
## Expertise
|
||||
- OAuth 2.0 / OIDC flows
|
||||
- JWT token handling
|
||||
- Session management
|
||||
- Security best practices
|
||||
|
||||
## Principles
|
||||
1. Security first - always consider attack vectors
|
||||
2. Maintain backwards compatibility
|
||||
3. All changes must have tests
|
||||
```
|
||||
|
||||
### memory.md (auto-updated)
|
||||
|
||||
```markdown
|
||||
# Auth Module Expert - Memory
|
||||
|
||||
## 2024-12-16
|
||||
|
||||
### Token Refresh Improvements
|
||||
- Implemented silent refresh with retry logic
|
||||
- Added 30-second buffer before expiry
|
||||
- New pattern: use `tokenManager.scheduleRefresh()`
|
||||
|
||||
## 2024-12-14
|
||||
|
||||
### Bug Fix: Race Condition
|
||||
- Fixed concurrent refresh calls issue
|
||||
- Added mutex lock in session manager
|
||||
```
|
||||
|
||||
## Watch Patterns
|
||||
|
||||
Agents watch for changes using glob patterns:
|
||||
|
||||
```yaml
|
||||
# .knowledge/agent-registry.yaml
|
||||
agents:
|
||||
- path: packages/shared-auth
|
||||
agent_dir: packages/shared-auth/.agent
|
||||
name: Auth Expert
|
||||
watches:
|
||||
- packages/shared-auth/** # All files in module
|
||||
- packages/core-utils/src/auth/** # Related code elsewhere
|
||||
- apps/*/middleware/auth.* # Auth middleware in apps
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Changes not being tracked
|
||||
|
||||
1. Check git hook is installed:
|
||||
```bash
|
||||
cat .git/hooks/post-commit | grep "Agent Knowledge"
|
||||
```
|
||||
|
||||
2. Reinstall hooks:
|
||||
```bash
|
||||
npx agent-knowledge setup-hooks --force
|
||||
```
|
||||
|
||||
### API errors during update
|
||||
|
||||
1. Check API key is set:
|
||||
```bash
|
||||
cat .knowledge/.env
|
||||
```
|
||||
|
||||
2. Test with different model:
|
||||
```bash
|
||||
npx agent-knowledge update --model anthropic/claude-3-haiku
|
||||
```
|
||||
|
||||
### Agent not receiving changes
|
||||
|
||||
Check watch patterns in registry:
|
||||
```bash
|
||||
npx agent-knowledge status
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
99
tools/agent-knowledge/bin/cli.js
Normal file
99
tools/agent-knowledge/bin/cli.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
import { initCommand } from '../src/commands/init.js';
|
||||
import { addAgentCommand } from '../src/commands/add-agent.js';
|
||||
import { updateCommand } from '../src/commands/update.js';
|
||||
import { statusCommand } from '../src/commands/status.js';
|
||||
import { setupHooksCommand } from '../src/commands/setup-hooks.js';
|
||||
import { scanCommand } from '../src/commands/scan.js';
|
||||
import { teamCommand } from '../src/commands/team.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
||||
|
||||
const program = new Command();
|
||||
|
||||
console.log(
|
||||
chalk.cyan(`
|
||||
╔═══════════════════════════════════════════╗
|
||||
║ ${chalk.bold('Agent Knowledge System')} v${pkg.version} ║
|
||||
║ AI agents that learn from your code ║
|
||||
╚═══════════════════════════════════════════╝
|
||||
`)
|
||||
);
|
||||
|
||||
program
|
||||
.name('agent-knowledge')
|
||||
.description('Auto-updating AI agent knowledge system for codebases')
|
||||
.version(pkg.version);
|
||||
|
||||
program
|
||||
.command('init')
|
||||
.description('Initialize agent knowledge system in current project')
|
||||
.option('-p, --provider <provider>', 'LLM provider (anthropic|openrouter)', 'openrouter')
|
||||
.option('-m, --model <model>', 'Default model to use', 'google/gemini-flash-1.5')
|
||||
.option('--no-hooks', 'Skip git hooks installation')
|
||||
.action(initCommand);
|
||||
|
||||
program
|
||||
.command('add-agent <path>')
|
||||
.description('Add an agent to a module/package (e.g., packages/shared-auth)')
|
||||
.option('-n, --name <name>', 'Agent display name')
|
||||
.option('-w, --watches <patterns...>', 'Additional watch patterns')
|
||||
.action(addAgentCommand);
|
||||
|
||||
program
|
||||
.command('update')
|
||||
.description('Manually trigger knowledge update for all agents')
|
||||
.option('-a, --agent <path>', 'Update specific agent only')
|
||||
.option('-m, --model <model>', 'Override model for this run')
|
||||
.option('--claude', 'Use Claude Code instead of external APIs')
|
||||
.option('--dry-run', 'Show what would be updated without making changes')
|
||||
.action(updateCommand);
|
||||
|
||||
program
|
||||
.command('status')
|
||||
.description('Show status of all agents and pending changes')
|
||||
.action(statusCommand);
|
||||
|
||||
program
|
||||
.command('setup-hooks')
|
||||
.description('Install/reinstall git hooks')
|
||||
.option('-f, --force', 'Overwrite existing hooks')
|
||||
.action(setupHooksCommand);
|
||||
|
||||
program
|
||||
.command('scan')
|
||||
.description('Scan project and discover modules for agents')
|
||||
.option('-d, --depth <n>', 'Max directory depth to scan', '4')
|
||||
.option('-a, --all', 'Show all discovered modules (not just top 5 per type)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--dry-run', 'Show what would be created without creating')
|
||||
.option('--claude', 'Generate Claude Code prompt for intelligent analysis')
|
||||
.option('--setup-all', 'Automatically create agents for all discovered modules')
|
||||
.option('--teams', 'Use team templates for apps (with --setup-all)')
|
||||
.option(
|
||||
'-t, --template <template>',
|
||||
'Team template for apps: startup, standard, enterprise',
|
||||
'startup'
|
||||
)
|
||||
.option('--init-claude', 'Generate prompts for Claude Code to create rich agent descriptions')
|
||||
.action(scanCommand);
|
||||
|
||||
program
|
||||
.command('team [path]')
|
||||
.description('Add a full development team to a project/module')
|
||||
.option('-t, --template <template>', 'Team template: startup, standard, enterprise', 'standard')
|
||||
.option('-l, --list', 'List available team templates')
|
||||
.option('--dry-run', 'Show what would be created')
|
||||
.action(teamCommand);
|
||||
|
||||
program.parse();
|
||||
29
tools/agent-knowledge/package.json
Normal file
29
tools/agent-knowledge/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "@manacore/agent-knowledge",
|
||||
"version": "1.0.0",
|
||||
"description": "Auto-updating AI agent knowledge system - agents that learn from your code changes",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"agent-knowledge": "bin/cli.js",
|
||||
"ak": "bin/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"cli": "node bin/cli.js",
|
||||
"update": "node bin/cli.js update --claude",
|
||||
"status": "node bin/cli.js status",
|
||||
"scan": "node bin/cli.js scan"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^9.2.23",
|
||||
"ora": "^8.0.1",
|
||||
"yaml": "^2.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
304
tools/agent-knowledge/src/commands/add-agent.js
Normal file
304
tools/agent-knowledge/src/commands/add-agent.js
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import inquirer from 'inquirer';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import yaml from 'yaml';
|
||||
|
||||
export async function addAgentCommand(modulePath, options) {
|
||||
const cwd = process.cwd();
|
||||
const fullPath = path.join(cwd, modulePath);
|
||||
const agentDir = path.join(fullPath, '.agent');
|
||||
|
||||
console.log(chalk.blue(`\n🤖 Adding agent for: ${modulePath}\n`));
|
||||
|
||||
// Validate path exists
|
||||
if (!(await fs.pathExists(fullPath))) {
|
||||
console.log(chalk.red(`Error: Path does not exist: ${fullPath}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if agent already exists
|
||||
if (await fs.pathExists(agentDir)) {
|
||||
const { overwrite } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'overwrite',
|
||||
message: '.agent directory already exists. Overwrite?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!overwrite) {
|
||||
console.log(chalk.yellow('Aborted.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create .knowledge directory if needed (Claude Code only mode)
|
||||
const knowledgeDir = path.join(cwd, '.knowledge');
|
||||
if (!(await fs.pathExists(knowledgeDir))) {
|
||||
await fs.ensureDir(knowledgeDir);
|
||||
await fs.writeFile(path.join(knowledgeDir, 'agent-registry.yaml'), 'agents: []\n');
|
||||
console.log(chalk.gray('Created .knowledge/ directory (Claude Code only mode)\n'));
|
||||
}
|
||||
|
||||
// Gather agent information
|
||||
const moduleName = path.basename(modulePath);
|
||||
const parentDir = path.basename(path.dirname(modulePath));
|
||||
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Agent display name:',
|
||||
default: options.name || `${formatName(moduleName)} Expert`,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'role',
|
||||
message: 'Agent role/title:',
|
||||
default: `${formatName(moduleName)} Specialist`,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'expertise',
|
||||
message: 'Primary expertise (comma-separated):',
|
||||
default: guessExpertise(moduleName),
|
||||
},
|
||||
{
|
||||
type: 'editor',
|
||||
name: 'description',
|
||||
message: 'Agent identity/description (opens editor):',
|
||||
default: `Deep expert in the ${moduleName} module. Understands all patterns, APIs, and implementation details.`,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'watches',
|
||||
message: 'Watch patterns (comma-separated, supports globs):',
|
||||
default: [modulePath + '/**', ...(options.watches || [])].join(', '),
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'scanForContext',
|
||||
message: 'Scan module for initial context (package.json, README, etc)?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const spinner = ora('Creating agent...').start();
|
||||
|
||||
try {
|
||||
// Create .agent directory
|
||||
await fs.ensureDir(agentDir);
|
||||
|
||||
// Scan for initial context if requested
|
||||
let initialContext = '';
|
||||
if (answers.scanForContext) {
|
||||
initialContext = await scanModuleContext(fullPath);
|
||||
}
|
||||
|
||||
// Create agent.md
|
||||
const agentContent = `# ${answers.name}
|
||||
|
||||
## Identity
|
||||
You are the **${answers.name}** for this codebase. ${answers.description}
|
||||
|
||||
## Role
|
||||
${answers.role}
|
||||
|
||||
## Expertise
|
||||
${answers.expertise
|
||||
.split(',')
|
||||
.map((e) => `- ${e.trim()}`)
|
||||
.join('\n')}
|
||||
|
||||
## Principles
|
||||
1. Understand existing patterns before suggesting changes
|
||||
2. Maintain consistency with the module's established conventions
|
||||
3. Consider impacts on consumers of this module
|
||||
4. Document breaking changes clearly
|
||||
5. Write tests for all changes
|
||||
|
||||
## Key Paths
|
||||
- Source: \`${modulePath}/src/\`
|
||||
- Tests: \`${modulePath}/test/\` or \`${modulePath}/__tests__/\`
|
||||
- Types: \`${modulePath}/types/\` or \`${modulePath}/src/types/\`
|
||||
|
||||
## How to Use This Agent
|
||||
When working on ${moduleName}:
|
||||
1. Read this file and memory.md for context
|
||||
2. Check architecture.md for structural understanding
|
||||
3. Reference memory.md for recent changes and decisions
|
||||
`;
|
||||
|
||||
await fs.writeFile(path.join(agentDir, 'agent.md'), agentContent);
|
||||
spinner.succeed('Created .agent/agent.md');
|
||||
|
||||
// Create memory.md
|
||||
spinner.start('Creating memory.md...');
|
||||
const memoryContent = `# ${answers.name} - Memory
|
||||
|
||||
This file is automatically updated with learnings from code changes.
|
||||
|
||||
## Recent Updates
|
||||
|
||||
*No updates yet. Memory will be populated after code changes are processed.*
|
||||
|
||||
---
|
||||
|
||||
## Historical Context
|
||||
|
||||
${initialContext || '*Initial context will be added here.*'}
|
||||
`;
|
||||
|
||||
await fs.writeFile(path.join(agentDir, 'memory.md'), memoryContent);
|
||||
spinner.succeed('Created .agent/memory.md');
|
||||
|
||||
// Create architecture.md
|
||||
spinner.start('Creating architecture.md...');
|
||||
const architectureContent = `# ${formatName(moduleName)} Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
*Document the high-level architecture of this module here.*
|
||||
|
||||
## Directory Structure
|
||||
|
||||
\`\`\`
|
||||
${modulePath}/
|
||||
├── src/ # Source code
|
||||
├── test/ # Tests
|
||||
├── types/ # TypeScript types (if applicable)
|
||||
└── package.json # Dependencies
|
||||
\`\`\`
|
||||
|
||||
## Key Components
|
||||
|
||||
*List and describe the main components/files here.*
|
||||
|
||||
## Dependencies
|
||||
|
||||
*Document key dependencies and why they're used.*
|
||||
|
||||
## Integration Points
|
||||
|
||||
*How other modules/apps interact with this module.*
|
||||
`;
|
||||
|
||||
await fs.writeFile(path.join(agentDir, 'architecture.md'), architectureContent);
|
||||
spinner.succeed('Created .agent/architecture.md');
|
||||
|
||||
// Update agent registry
|
||||
spinner.start('Updating agent registry...');
|
||||
await updateRegistry(cwd, {
|
||||
path: modulePath,
|
||||
agent_dir: path.join(modulePath, '.agent'),
|
||||
name: answers.name,
|
||||
watches: answers.watches.split(',').map((w) => w.trim()),
|
||||
});
|
||||
spinner.succeed('Updated .knowledge/agent-registry.yaml');
|
||||
|
||||
console.log(chalk.green(`\n✅ Agent created successfully at ${modulePath}/.agent/\n`));
|
||||
|
||||
console.log(chalk.cyan('Files created:'));
|
||||
console.log(chalk.gray(` ${modulePath}/.agent/agent.md - Agent persona & instructions`));
|
||||
console.log(chalk.gray(` ${modulePath}/.agent/memory.md - Auto-updated knowledge`));
|
||||
console.log(
|
||||
chalk.gray(` ${modulePath}/.agent/architecture.md - Architecture docs (edit manually)`)
|
||||
);
|
||||
|
||||
console.log(chalk.cyan('\nNext steps:'));
|
||||
console.log(chalk.white(' 1. Review and customize .agent/agent.md'));
|
||||
console.log(chalk.white(' 2. Document architecture in .agent/architecture.md'));
|
||||
console.log(chalk.white(' 3. Make commits - changes will be tracked automatically'));
|
||||
console.log(chalk.white(' 4. Run update to populate memory:'));
|
||||
console.log(chalk.gray(' npx agent-knowledge update\n'));
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to create agent');
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function formatName(name) {
|
||||
return name
|
||||
.split(/[-_]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function guessExpertise(moduleName) {
|
||||
const expertiseMap = {
|
||||
auth: 'Authentication, Authorization, OAuth, JWT, Sessions, Security',
|
||||
ui: 'React Components, Design System, Accessibility, Styling',
|
||||
api: 'REST APIs, GraphQL, HTTP, Request Handling, Middleware',
|
||||
database: 'SQL, Migrations, ORM, Query Optimization, Data Modeling',
|
||||
utils: 'Utility Functions, Helpers, Common Patterns',
|
||||
core: 'Core Business Logic, Domain Models, Validation',
|
||||
config: 'Configuration Management, Environment Variables, Feature Flags',
|
||||
};
|
||||
|
||||
for (const [key, expertise] of Object.entries(expertiseMap)) {
|
||||
if (moduleName.toLowerCase().includes(key)) {
|
||||
return expertise;
|
||||
}
|
||||
}
|
||||
|
||||
return 'Module internals, APIs, Patterns, Best Practices';
|
||||
}
|
||||
|
||||
async function scanModuleContext(modulePath) {
|
||||
let context = '';
|
||||
|
||||
// Check for package.json
|
||||
const packagePath = path.join(modulePath, 'package.json');
|
||||
if (await fs.pathExists(packagePath)) {
|
||||
const pkg = await fs.readJson(packagePath);
|
||||
context += `### Package Info\n`;
|
||||
context += `- Name: ${pkg.name || 'unknown'}\n`;
|
||||
context += `- Version: ${pkg.version || 'unknown'}\n`;
|
||||
if (pkg.description) {
|
||||
context += `- Description: ${pkg.description}\n`;
|
||||
}
|
||||
if (pkg.dependencies) {
|
||||
context += `- Key dependencies: ${Object.keys(pkg.dependencies).slice(0, 5).join(', ')}\n`;
|
||||
}
|
||||
context += '\n';
|
||||
}
|
||||
|
||||
// Check for README
|
||||
const readmePaths = ['README.md', 'readme.md', 'README.MD'];
|
||||
for (const readmeName of readmePaths) {
|
||||
const readmePath = path.join(modulePath, readmeName);
|
||||
if (await fs.pathExists(readmePath)) {
|
||||
const readme = await fs.readFile(readmePath, 'utf-8');
|
||||
// Take first 500 chars
|
||||
context += `### From README\n`;
|
||||
context += readme.substring(0, 500).trim();
|
||||
if (readme.length > 500) context += '...';
|
||||
context += '\n\n';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
async function updateRegistry(cwd, agent) {
|
||||
const registryPath = path.join(cwd, '.knowledge', 'agent-registry.yaml');
|
||||
let registry = { agents: [] };
|
||||
|
||||
if (await fs.pathExists(registryPath)) {
|
||||
const content = await fs.readFile(registryPath, 'utf-8');
|
||||
registry = yaml.parse(content) || { agents: [] };
|
||||
}
|
||||
|
||||
// Remove existing entry for this path
|
||||
registry.agents = registry.agents.filter((a) => a.path !== agent.path);
|
||||
|
||||
// Add new entry
|
||||
registry.agents.push(agent);
|
||||
|
||||
await fs.writeFile(registryPath, yaml.stringify(registry));
|
||||
}
|
||||
361
tools/agent-knowledge/src/commands/init.js
Normal file
361
tools/agent-knowledge/src/commands/init.js
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import inquirer from 'inquirer';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates');
|
||||
|
||||
export async function initCommand(options) {
|
||||
const cwd = process.cwd();
|
||||
|
||||
console.log(chalk.blue('\n📦 Initializing Agent Knowledge System...\n'));
|
||||
|
||||
// Check if already initialized
|
||||
if (await fs.pathExists(path.join(cwd, '.knowledge'))) {
|
||||
const { overwrite } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'overwrite',
|
||||
message: '.knowledge directory already exists. Reinitialize?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!overwrite) {
|
||||
console.log(chalk.yellow('Aborted.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Gather configuration
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'provider',
|
||||
message: 'Select your primary LLM provider:',
|
||||
choices: [
|
||||
{ name: 'OpenRouter (recommended - access to many models)', value: 'openrouter' },
|
||||
{ name: 'Anthropic (Claude direct)', value: 'anthropic' },
|
||||
],
|
||||
default: options.provider,
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'model',
|
||||
message: 'Select default model for knowledge updates:',
|
||||
choices: (ans) =>
|
||||
ans.provider === 'openrouter'
|
||||
? [
|
||||
{
|
||||
name: 'google/gemini-flash-1.5 (cheapest - $0.075/1M)',
|
||||
value: 'google/gemini-flash-1.5',
|
||||
},
|
||||
{
|
||||
name: 'deepseek/deepseek-chat (very cheap - $0.14/1M)',
|
||||
value: 'deepseek/deepseek-chat',
|
||||
},
|
||||
{
|
||||
name: 'anthropic/claude-3-haiku (reliable - $0.25/1M)',
|
||||
value: 'anthropic/claude-3-haiku',
|
||||
},
|
||||
{
|
||||
name: 'anthropic/claude-sonnet-4-20250514 (best quality - $3/1M)',
|
||||
value: 'anthropic/claude-sonnet-4-20250514',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{ name: 'claude-3-haiku-20240307 (fast)', value: 'claude-3-haiku-20240307' },
|
||||
{ name: 'claude-sonnet-4-20250514 (balanced)', value: 'claude-sonnet-4-20250514' },
|
||||
],
|
||||
default: options.model,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'apiKey',
|
||||
message: (ans) =>
|
||||
`Enter your ${ans.provider === 'openrouter' ? 'OpenRouter' : 'Anthropic'} API key:`,
|
||||
validate: (input) => input.length > 10 || 'Please enter a valid API key',
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'installHooks',
|
||||
message: 'Install git hooks for automatic change tracking?',
|
||||
default: options.hooks !== false,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'addToGitignore',
|
||||
message: 'Add sensitive files to .gitignore?',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'createClaudeConfig',
|
||||
message: 'Create CLAUDE.md configuration for Claude Code integration?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const spinner = ora('Creating directory structure...').start();
|
||||
|
||||
try {
|
||||
// 1. Create .knowledge directory structure
|
||||
await fs.ensureDir(path.join(cwd, '.knowledge', 'archive'));
|
||||
|
||||
// 2. Create config file
|
||||
const config = {
|
||||
version: '1.0.0',
|
||||
provider: answers.provider,
|
||||
model: answers.model,
|
||||
models: {
|
||||
batch: {
|
||||
provider: answers.provider,
|
||||
model: answers.model,
|
||||
},
|
||||
quality: {
|
||||
provider: answers.provider,
|
||||
model:
|
||||
answers.provider === 'openrouter'
|
||||
? 'anthropic/claude-sonnet-4-20250514'
|
||||
: 'claude-sonnet-4-20250514',
|
||||
},
|
||||
fallbacks:
|
||||
answers.provider === 'openrouter'
|
||||
? [
|
||||
{ provider: 'openrouter', model: 'anthropic/claude-3-haiku' },
|
||||
{ provider: 'openrouter', model: 'google/gemini-flash-1.5' },
|
||||
]
|
||||
: [],
|
||||
},
|
||||
budget: {
|
||||
daily_limit_usd: 1.0,
|
||||
alert_threshold: 0.8,
|
||||
},
|
||||
};
|
||||
|
||||
await fs.writeFile(path.join(cwd, '.knowledge', 'config.yaml'), generateYaml(config));
|
||||
spinner.succeed('Created .knowledge/config.yaml');
|
||||
|
||||
// 3. Create agent registry
|
||||
spinner.start('Creating agent registry...');
|
||||
const registry = {
|
||||
agents: [],
|
||||
};
|
||||
await fs.writeFile(path.join(cwd, '.knowledge', 'agent-registry.yaml'), generateYaml(registry));
|
||||
spinner.succeed('Created .knowledge/agent-registry.yaml');
|
||||
|
||||
// 4. Create empty changes file
|
||||
await fs.writeFile(path.join(cwd, '.knowledge', 'changes.jsonl'), '');
|
||||
|
||||
// 5. Create .env file for API key
|
||||
spinner.start('Creating environment file...');
|
||||
const envContent =
|
||||
answers.provider === 'openrouter'
|
||||
? `# Agent Knowledge System Configuration
|
||||
OPENROUTER_API_KEY=${answers.apiKey}
|
||||
LLM_PROVIDER=openrouter
|
||||
OPENROUTER_MODEL=${answers.model}
|
||||
`
|
||||
: `# Agent Knowledge System Configuration
|
||||
ANTHROPIC_API_KEY=${answers.apiKey}
|
||||
LLM_PROVIDER=anthropic
|
||||
ANTHROPIC_MODEL=${answers.model}
|
||||
`;
|
||||
await fs.writeFile(path.join(cwd, '.knowledge', '.env'), envContent);
|
||||
spinner.succeed('Created .knowledge/.env');
|
||||
|
||||
// 6. Install git hooks
|
||||
if (answers.installHooks) {
|
||||
spinner.start('Installing git hooks...');
|
||||
await installGitHooks(cwd);
|
||||
spinner.succeed('Installed git hooks');
|
||||
}
|
||||
|
||||
// 7. Update .gitignore
|
||||
if (answers.addToGitignore) {
|
||||
spinner.start('Updating .gitignore...');
|
||||
await updateGitignore(cwd);
|
||||
spinner.succeed('Updated .gitignore');
|
||||
}
|
||||
|
||||
// 8. Create CLAUDE.md integration
|
||||
if (answers.createClaudeConfig) {
|
||||
spinner.start('Creating Claude Code integration...');
|
||||
await createClaudeConfig(cwd);
|
||||
spinner.succeed('Created/updated CLAUDE.md');
|
||||
}
|
||||
|
||||
// 9. Create update script
|
||||
spinner.start('Creating update scripts...');
|
||||
await fs.ensureDir(path.join(cwd, 'scripts', 'agents'));
|
||||
await fs.copyFile(
|
||||
path.join(TEMPLATES_DIR, 'update-agents.js'),
|
||||
path.join(cwd, 'scripts', 'agents', 'update-agents.js')
|
||||
);
|
||||
spinner.succeed('Created scripts/agents/update-agents.js');
|
||||
|
||||
console.log(chalk.green('\n✅ Agent Knowledge System initialized successfully!\n'));
|
||||
|
||||
console.log(chalk.cyan('Next steps:'));
|
||||
console.log(chalk.white(' 1. Add agents to your modules:'));
|
||||
console.log(chalk.gray(' npx agent-knowledge add-agent packages/shared-auth'));
|
||||
console.log(chalk.white(' 2. Make some commits and watch changes being tracked'));
|
||||
console.log(chalk.white(' 3. Run manual update or set up cron job:'));
|
||||
console.log(chalk.gray(' npx agent-knowledge update'));
|
||||
console.log(chalk.white(' 4. Set up nightly cron (optional):'));
|
||||
console.log(chalk.gray(' 0 3 * * * cd /your/project && npx agent-knowledge update\n'));
|
||||
} catch (error) {
|
||||
spinner.fail('Initialization failed');
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function installGitHooks(cwd) {
|
||||
const hooksDir = path.join(cwd, '.git', 'hooks');
|
||||
|
||||
if (!(await fs.pathExists(path.join(cwd, '.git')))) {
|
||||
console.log(chalk.yellow(' Warning: Not a git repository, skipping hooks'));
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.ensureDir(hooksDir);
|
||||
|
||||
const hookContent = `#!/bin/bash
|
||||
# Agent Knowledge System - Change Tracker
|
||||
# Appends commit changes to .knowledge/changes.jsonl
|
||||
|
||||
CHANGES_FILE=".knowledge/changes.jsonl"
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
COMMIT_SHA=$(git rev-parse HEAD)
|
||||
COMMIT_MSG=$(git log -1 --pretty=format:"%s" | sed 's/"/\\\\"/g')
|
||||
AUTHOR=$(git log -1 --pretty=format:"%an")
|
||||
BRANCH=$(git branch --show-current)
|
||||
|
||||
# Only track if .knowledge exists
|
||||
if [ ! -d ".knowledge" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get changed files
|
||||
git diff-tree --no-commit-id --name-status -r HEAD | while read status file; do
|
||||
# Determine module from path
|
||||
MODULE=$(echo "$file" | cut -d'/' -f1-2)
|
||||
|
||||
# Get truncated diff
|
||||
DIFF=$(git show HEAD -- "$file" 2>/dev/null | head -50 | sed 's/"/\\\\"/g' | tr '\\n' '\\\\n' | head -c 2000)
|
||||
|
||||
# Append to changes log
|
||||
echo "{\\"timestamp\\":\\"$TIMESTAMP\\",\\"commit\\":\\"$COMMIT_SHA\\",\\"message\\":\\"$COMMIT_MSG\\",\\"author\\":\\"$AUTHOR\\",\\"branch\\":\\"$BRANCH\\",\\"module\\":\\"$MODULE\\",\\"file\\":\\"$file\\",\\"status\\":\\"$status\\",\\"diff\\":\\"$DIFF\\"}" >> "$CHANGES_FILE"
|
||||
done
|
||||
|
||||
echo "📝 Changes logged for agent knowledge system"
|
||||
`;
|
||||
|
||||
const hookPath = path.join(hooksDir, 'post-commit');
|
||||
|
||||
// Check if hook exists and has other content
|
||||
if (await fs.pathExists(hookPath)) {
|
||||
const existing = await fs.readFile(hookPath, 'utf-8');
|
||||
if (!existing.includes('Agent Knowledge System')) {
|
||||
// Append to existing hook
|
||||
await fs.appendFile(hookPath, '\n\n' + hookContent);
|
||||
}
|
||||
} else {
|
||||
await fs.writeFile(hookPath, hookContent);
|
||||
}
|
||||
|
||||
await fs.chmod(hookPath, '755');
|
||||
}
|
||||
|
||||
async function updateGitignore(cwd) {
|
||||
const gitignorePath = path.join(cwd, '.gitignore');
|
||||
const additions = `
|
||||
# Agent Knowledge System
|
||||
.knowledge/.env
|
||||
.knowledge/archive/
|
||||
`;
|
||||
|
||||
if (await fs.pathExists(gitignorePath)) {
|
||||
const existing = await fs.readFile(gitignorePath, 'utf-8');
|
||||
if (!existing.includes('Agent Knowledge System')) {
|
||||
await fs.appendFile(gitignorePath, additions);
|
||||
}
|
||||
} else {
|
||||
await fs.writeFile(gitignorePath, additions.trim());
|
||||
}
|
||||
}
|
||||
|
||||
async function createClaudeConfig(cwd) {
|
||||
const claudePath = path.join(cwd, 'CLAUDE.md');
|
||||
|
||||
const agentSection = `
|
||||
## Agent Knowledge System
|
||||
|
||||
This project uses specialized AI agents with auto-updating knowledge. When working on a specific module:
|
||||
|
||||
1. Check if \`.agent/agent.md\` exists in the directory you're working in
|
||||
2. If found, read all files in \`.agent/\` to load domain knowledge
|
||||
3. Adopt that agent's persona, expertise, and principles
|
||||
4. Reference the memory.md for recent changes and context
|
||||
|
||||
### Finding Agents
|
||||
|
||||
Search for agents with: \`find . -name "agent.md" -path "*/.agent/*"\`
|
||||
|
||||
### Agent Structure
|
||||
|
||||
Each \`.agent/\` folder contains:
|
||||
- \`agent.md\` - Persona, role, expertise, principles
|
||||
- \`memory.md\` - Auto-updated knowledge from code changes
|
||||
- \`architecture.md\` - Module architecture (manual)
|
||||
- Additional context files as needed
|
||||
`;
|
||||
|
||||
if (await fs.pathExists(claudePath)) {
|
||||
const existing = await fs.readFile(claudePath, 'utf-8');
|
||||
if (!existing.includes('Agent Knowledge System')) {
|
||||
await fs.appendFile(claudePath, '\n' + agentSection);
|
||||
}
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
claudePath,
|
||||
`# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
${agentSection}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function generateYaml(obj, indent = 0) {
|
||||
let yaml = '';
|
||||
const spaces = ' '.repeat(indent);
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (Array.isArray(value)) {
|
||||
yaml += `${spaces}${key}:\n`;
|
||||
for (const item of value) {
|
||||
if (typeof item === 'object') {
|
||||
yaml += `${spaces} -\n`;
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
yaml += `${spaces} ${k}: ${v}\n`;
|
||||
}
|
||||
} else {
|
||||
yaml += `${spaces} - ${item}\n`;
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
yaml += `${spaces}${key}:\n`;
|
||||
yaml += generateYaml(value, indent + 1);
|
||||
} else {
|
||||
yaml += `${spaces}${key}: ${value}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return yaml;
|
||||
}
|
||||
1730
tools/agent-knowledge/src/commands/scan.js
Normal file
1730
tools/agent-knowledge/src/commands/scan.js
Normal file
File diff suppressed because it is too large
Load diff
122
tools/agent-knowledge/src/commands/setup-hooks.js
Normal file
122
tools/agent-knowledge/src/commands/setup-hooks.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export async function setupHooksCommand(options) {
|
||||
const cwd = process.cwd();
|
||||
|
||||
console.log(chalk.blue('\n🔧 Setting up Git Hooks...\n'));
|
||||
|
||||
// Check if git repo
|
||||
const gitPath = path.join(cwd, '.git');
|
||||
if (!(await fs.pathExists(gitPath))) {
|
||||
console.log(chalk.red('Error: Not a git repository'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Handle worktrees: .git can be a file pointing to the actual gitdir
|
||||
let hooksDir;
|
||||
const gitStat = await fs.stat(gitPath);
|
||||
|
||||
if (gitStat.isFile()) {
|
||||
// It's a worktree - read the gitdir reference
|
||||
const gitContent = await fs.readFile(gitPath, 'utf-8');
|
||||
const match = gitContent.match(/gitdir:\s*(.+)/);
|
||||
if (match) {
|
||||
const worktreeGitDir = match[1].trim();
|
||||
// For worktrees, hooks are in the common dir (main repo's .git/hooks)
|
||||
// The worktree gitdir is like: /path/to/main/.git/worktrees/branch-name
|
||||
// We need: /path/to/main/.git/hooks
|
||||
const mainGitDir = path.resolve(worktreeGitDir, '..', '..');
|
||||
hooksDir = path.join(mainGitDir, 'hooks');
|
||||
console.log(chalk.gray(`Detected worktree, using hooks from: ${hooksDir}\n`));
|
||||
} else {
|
||||
console.log(chalk.red('Error: Could not parse .git file'));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Normal repo
|
||||
hooksDir = path.join(cwd, '.git', 'hooks');
|
||||
}
|
||||
|
||||
const spinner = ora('Installing hooks...').start();
|
||||
|
||||
try {
|
||||
await fs.ensureDir(hooksDir);
|
||||
|
||||
const hookContent = `#!/bin/bash
|
||||
# Agent Knowledge System - Change Tracker
|
||||
# Automatically logs code changes for AI agent knowledge updates
|
||||
|
||||
CHANGES_FILE=".knowledge/changes.jsonl"
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
COMMIT_SHA=$(git rev-parse HEAD)
|
||||
COMMIT_MSG=$(git log -1 --pretty=format:"%s" | sed 's/"/\\\\"/g')
|
||||
AUTHOR=$(git log -1 --pretty=format:"%an")
|
||||
BRANCH=$(git branch --show-current)
|
||||
|
||||
# Only track if .knowledge directory exists
|
||||
if [ ! -d ".knowledge" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get changed files and log them
|
||||
git diff-tree --no-commit-id --name-status -r HEAD | while IFS=$'\\t' read -r status file; do
|
||||
# Skip if file is empty
|
||||
[ -z "$file" ] && continue
|
||||
|
||||
# Determine module from path (first two path segments)
|
||||
MODULE=$(echo "$file" | cut -d'/' -f1-2)
|
||||
|
||||
# Get truncated diff for context (max 2000 chars)
|
||||
DIFF=$(git show HEAD -- "$file" 2>/dev/null | head -50 | sed 's/"/\\\\"/g' | tr '\\n' ' ' | cut -c1-2000)
|
||||
|
||||
# Create JSON entry and append
|
||||
echo "{\\"timestamp\\":\\"$TIMESTAMP\\",\\"commit\\":\\"$COMMIT_SHA\\",\\"message\\":\\"$COMMIT_MSG\\",\\"author\\":\\"$AUTHOR\\",\\"branch\\":\\"$BRANCH\\",\\"module\\":\\"$MODULE\\",\\"file\\":\\"$file\\",\\"status\\":\\"$status\\",\\"diff\\":\\"$DIFF\\"}" >> "$CHANGES_FILE"
|
||||
done
|
||||
|
||||
# Notify user
|
||||
CHANGE_COUNT=$(git diff-tree --no-commit-id --name-only -r HEAD | wc -l | tr -d ' ')
|
||||
if [ "$CHANGE_COUNT" -gt 0 ]; then
|
||||
echo "📝 Tracked $CHANGE_COUNT file(s) for agent knowledge"
|
||||
fi
|
||||
`;
|
||||
|
||||
const hookPath = path.join(hooksDir, 'post-commit');
|
||||
|
||||
// Check if hook already exists
|
||||
if (await fs.pathExists(hookPath)) {
|
||||
const existing = await fs.readFile(hookPath, 'utf-8');
|
||||
|
||||
if (existing.includes('Agent Knowledge System')) {
|
||||
if (options.force) {
|
||||
// Replace the AKS section
|
||||
const beforeAKS = existing.split('# Agent Knowledge System')[0];
|
||||
await fs.writeFile(hookPath, beforeAKS.trim() + '\n\n' + hookContent);
|
||||
spinner.succeed('Replaced existing Agent Knowledge System hook');
|
||||
} else {
|
||||
spinner.info('Hook already installed. Use --force to reinstall.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Append to existing hook
|
||||
await fs.appendFile(hookPath, '\n\n' + hookContent);
|
||||
spinner.succeed('Appended to existing post-commit hook');
|
||||
}
|
||||
} else {
|
||||
await fs.writeFile(hookPath, hookContent);
|
||||
spinner.succeed('Created post-commit hook');
|
||||
}
|
||||
|
||||
// Make executable
|
||||
await fs.chmod(hookPath, '755');
|
||||
|
||||
console.log(chalk.green('\n✅ Git hooks installed successfully!\n'));
|
||||
console.log(chalk.gray('Changes will now be tracked automatically on each commit.'));
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to install hooks');
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
131
tools/agent-knowledge/src/commands/status.js
Normal file
131
tools/agent-knowledge/src/commands/status.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import yaml from 'yaml';
|
||||
|
||||
export async function statusCommand() {
|
||||
const cwd = process.cwd();
|
||||
const knowledgeDir = path.join(cwd, '.knowledge');
|
||||
|
||||
console.log(chalk.blue('\n📊 Agent Knowledge System Status\n'));
|
||||
|
||||
// Check initialization
|
||||
if (!(await fs.pathExists(knowledgeDir))) {
|
||||
console.log(chalk.red('❌ Not initialized'));
|
||||
console.log(chalk.gray('Run: npx agent-knowledge init'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.green('✓ Initialized\n'));
|
||||
|
||||
// Load config
|
||||
const configPath = path.join(knowledgeDir, 'config.yaml');
|
||||
if (await fs.pathExists(configPath)) {
|
||||
const config = yaml.parse(await fs.readFile(configPath, 'utf-8'));
|
||||
console.log(chalk.cyan('Configuration:'));
|
||||
console.log(chalk.gray(` Provider: ${config.provider}`));
|
||||
console.log(chalk.gray(` Model: ${config.model}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Load registry
|
||||
const registryPath = path.join(knowledgeDir, 'agent-registry.yaml');
|
||||
const registry = yaml.parse(await fs.readFile(registryPath, 'utf-8'));
|
||||
|
||||
console.log(chalk.cyan(`Registered Agents (${registry.agents.length}):`));
|
||||
|
||||
if (registry.agents.length === 0) {
|
||||
console.log(chalk.gray(' No agents registered'));
|
||||
console.log(chalk.gray(' Add one with: npx agent-knowledge add-agent <path>'));
|
||||
} else {
|
||||
for (const agent of registry.agents) {
|
||||
const agentDir = path.join(cwd, agent.agent_dir);
|
||||
const memoryPath = path.join(agentDir, 'memory.md');
|
||||
|
||||
let status = '✓';
|
||||
let memoryInfo = '';
|
||||
|
||||
if (!(await fs.pathExists(agentDir))) {
|
||||
status = '❌';
|
||||
memoryInfo = chalk.red('(missing)');
|
||||
} else if (await fs.pathExists(memoryPath)) {
|
||||
const stats = await fs.stat(memoryPath);
|
||||
const lastUpdate = stats.mtime.toLocaleDateString();
|
||||
memoryInfo = chalk.gray(`(updated: ${lastUpdate})`);
|
||||
} else {
|
||||
memoryInfo = chalk.yellow('(no memory yet)');
|
||||
}
|
||||
|
||||
console.log(` ${status} ${chalk.white(agent.name)}`);
|
||||
console.log(` ${chalk.gray('Path:')} ${agent.path}`);
|
||||
console.log(` ${chalk.gray('Memory:')} ${memoryInfo}`);
|
||||
console.log(` ${chalk.gray('Watches:')} ${agent.watches.length} patterns`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check pending changes
|
||||
console.log('');
|
||||
const changesPath = path.join(knowledgeDir, 'changes.jsonl');
|
||||
let pendingChanges = 0;
|
||||
|
||||
if (await fs.pathExists(changesPath)) {
|
||||
const content = await fs.readFile(changesPath, 'utf-8');
|
||||
pendingChanges = content.split('\n').filter((line) => line.trim()).length;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Pending Changes:'));
|
||||
if (pendingChanges === 0) {
|
||||
console.log(chalk.gray(' No pending changes'));
|
||||
} else {
|
||||
console.log(chalk.yellow(` ${pendingChanges} changes waiting to be processed`));
|
||||
console.log(chalk.gray(' Run: npx agent-knowledge update'));
|
||||
}
|
||||
|
||||
// Check git hooks (handle worktrees)
|
||||
console.log('');
|
||||
console.log(chalk.cyan('Git Hooks:'));
|
||||
|
||||
let hookPath;
|
||||
const gitPath = path.join(cwd, '.git');
|
||||
const gitStat = await fs.stat(gitPath).catch(() => null);
|
||||
|
||||
if (gitStat?.isFile()) {
|
||||
// It's a worktree - parse gitdir reference
|
||||
const gitContent = await fs.readFile(gitPath, 'utf-8');
|
||||
const match = gitContent.match(/gitdir:\s*(.+)/);
|
||||
if (match) {
|
||||
const worktreeGitDir = match[1].trim();
|
||||
// Hooks are in the main repo's .git/hooks
|
||||
const mainGitDir = path.resolve(worktreeGitDir, '..', '..');
|
||||
hookPath = path.join(mainGitDir, 'hooks', 'post-commit');
|
||||
}
|
||||
} else {
|
||||
hookPath = path.join(cwd, '.git', 'hooks', 'post-commit');
|
||||
}
|
||||
|
||||
if (hookPath && (await fs.pathExists(hookPath))) {
|
||||
const hookContent = await fs.readFile(hookPath, 'utf-8');
|
||||
if (hookContent.includes('Agent Knowledge System')) {
|
||||
console.log(chalk.green(' ✓ post-commit hook installed'));
|
||||
} else {
|
||||
console.log(chalk.yellow(' ⚠ post-commit hook exists but missing AKS integration'));
|
||||
console.log(chalk.gray(' Run: npx agent-knowledge setup-hooks'));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.yellow(' ⚠ post-commit hook not installed'));
|
||||
console.log(chalk.gray(' Run: npx agent-knowledge setup-hooks'));
|
||||
}
|
||||
|
||||
// Archive stats
|
||||
console.log('');
|
||||
const archiveDir = path.join(knowledgeDir, 'archive');
|
||||
if (await fs.pathExists(archiveDir)) {
|
||||
const archives = await fs.readdir(archiveDir);
|
||||
if (archives.length > 0) {
|
||||
console.log(chalk.cyan('Archive:'));
|
||||
console.log(chalk.gray(` ${archives.length} archived change files`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
996
tools/agent-knowledge/src/commands/team.js
Normal file
996
tools/agent-knowledge/src/commands/team.js
Normal file
|
|
@ -0,0 +1,996 @@
|
|||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import inquirer from 'inquirer';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import yaml from 'yaml';
|
||||
|
||||
// Team templates based on software development best practices
|
||||
const TEAM_TEMPLATES = {
|
||||
startup: {
|
||||
name: 'Startup Team',
|
||||
description: 'Lean team for small projects and MVPs (3 agents)',
|
||||
roles: [
|
||||
{
|
||||
id: 'tech-lead',
|
||||
name: 'Tech Lead',
|
||||
icon: '👨💻',
|
||||
role: 'Technical Leader & Senior Developer',
|
||||
identity:
|
||||
'Experienced technical leader who makes architecture decisions, reviews code, and handles complex implementation. Balances pragmatism with best practices.',
|
||||
expertise: [
|
||||
'Architecture decisions',
|
||||
'Code review',
|
||||
'Technical debt management',
|
||||
'Performance optimization',
|
||||
'Mentoring and knowledge sharing',
|
||||
],
|
||||
principles: [
|
||||
'Ship fast but maintain quality',
|
||||
'Make reversible decisions quickly',
|
||||
'Document critical decisions',
|
||||
'Keep the codebase simple and maintainable',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'developer',
|
||||
name: 'Developer',
|
||||
icon: '💻',
|
||||
role: 'Full-Stack Developer',
|
||||
identity:
|
||||
'Versatile developer who can work across the stack. Focused on shipping features while maintaining code quality.',
|
||||
expertise: [
|
||||
'Feature implementation',
|
||||
'Bug fixes',
|
||||
'API development',
|
||||
'Frontend development',
|
||||
'Database operations',
|
||||
],
|
||||
principles: [
|
||||
'Write clean, readable code',
|
||||
'Test critical paths',
|
||||
'Ask questions early',
|
||||
'Refactor as you go',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'qa',
|
||||
name: 'QA Engineer',
|
||||
icon: '🧪',
|
||||
role: 'Quality Assurance Specialist',
|
||||
identity:
|
||||
'Quality guardian who ensures the product works correctly. Thinks about edge cases and user experience.',
|
||||
expertise: [
|
||||
'Test planning',
|
||||
'Manual testing',
|
||||
'Automated testing',
|
||||
'Bug reporting',
|
||||
'User experience validation',
|
||||
],
|
||||
principles: [
|
||||
'Think like a user',
|
||||
'Test edge cases',
|
||||
'Automate repetitive tests',
|
||||
'Report bugs with clear reproduction steps',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
standard: {
|
||||
name: 'Standard Team',
|
||||
description: 'Balanced team for most projects (6 agents)',
|
||||
roles: [
|
||||
{
|
||||
id: 'product-owner',
|
||||
name: 'Product Owner',
|
||||
icon: '📋',
|
||||
role: 'Product Strategy & Requirements Specialist',
|
||||
identity:
|
||||
'Voice of the customer and business. Defines what to build, prioritizes features, and ensures the product delivers value.',
|
||||
expertise: [
|
||||
'Requirements gathering',
|
||||
'User story writing',
|
||||
'Feature prioritization',
|
||||
'Stakeholder communication',
|
||||
'Product roadmap planning',
|
||||
],
|
||||
principles: [
|
||||
'Focus on user value',
|
||||
'Prioritize ruthlessly',
|
||||
'Define clear acceptance criteria',
|
||||
'Balance business needs with technical constraints',
|
||||
],
|
||||
tools: ['Can use Perplexity for market research and competitive analysis'],
|
||||
},
|
||||
{
|
||||
id: 'architect',
|
||||
name: 'Architect',
|
||||
icon: '🏗️',
|
||||
role: 'System Architecture & Design Specialist',
|
||||
identity:
|
||||
'Designs the overall system structure. Makes decisions about patterns, technologies, and how components interact.',
|
||||
expertise: [
|
||||
'System design',
|
||||
'API design',
|
||||
'Database modeling',
|
||||
'Integration patterns',
|
||||
'Scalability planning',
|
||||
'Technology selection',
|
||||
],
|
||||
principles: [
|
||||
'Design for change',
|
||||
'Keep it simple until complexity is needed',
|
||||
'Document architectural decisions (ADRs)',
|
||||
'Consider non-functional requirements',
|
||||
'Plan for failure',
|
||||
],
|
||||
tools: ['Can use Perplexity for researching best practices and patterns'],
|
||||
},
|
||||
{
|
||||
id: 'senior-dev',
|
||||
name: 'Senior Developer',
|
||||
icon: '👨💻',
|
||||
role: 'Senior Software Engineer',
|
||||
identity:
|
||||
'Experienced developer who handles complex features, mentors others, and ensures code quality through reviews.',
|
||||
expertise: [
|
||||
'Complex feature implementation',
|
||||
'Code review',
|
||||
'Performance optimization',
|
||||
'Technical debt reduction',
|
||||
'Debugging complex issues',
|
||||
],
|
||||
principles: [
|
||||
'Write self-documenting code',
|
||||
'Review code thoroughly but constructively',
|
||||
'Mentor through code examples',
|
||||
'Balance perfection with pragmatism',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'developer',
|
||||
name: 'Developer',
|
||||
icon: '💻',
|
||||
role: 'Software Developer',
|
||||
identity:
|
||||
'Core implementer who turns designs into working code. Focused on feature delivery and bug fixing.',
|
||||
expertise: [
|
||||
'Feature implementation',
|
||||
'Bug fixes',
|
||||
'Unit testing',
|
||||
'API integration',
|
||||
'Code documentation',
|
||||
],
|
||||
principles: [
|
||||
'Follow established patterns',
|
||||
'Write tests for your code',
|
||||
'Ask for help when stuck',
|
||||
'Keep commits small and focused',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
name: 'Security Engineer',
|
||||
icon: '🔒',
|
||||
role: 'Application Security Specialist',
|
||||
identity:
|
||||
'Security guardian who identifies vulnerabilities, reviews code for security issues, and ensures secure practices.',
|
||||
expertise: [
|
||||
'Security code review',
|
||||
'OWASP Top 10',
|
||||
'Authentication & authorization',
|
||||
'Input validation',
|
||||
'Secure API design',
|
||||
'Dependency vulnerability scanning',
|
||||
],
|
||||
principles: [
|
||||
'Security is everyones responsibility',
|
||||
'Defense in depth',
|
||||
'Least privilege principle',
|
||||
'Never trust user input',
|
||||
'Keep dependencies updated',
|
||||
],
|
||||
tools: ['Can use Perplexity for CVE research and security best practices'],
|
||||
},
|
||||
{
|
||||
id: 'qa-lead',
|
||||
name: 'QA Lead',
|
||||
icon: '🧪',
|
||||
role: 'Quality Assurance Lead',
|
||||
identity:
|
||||
'Quality strategist who designs test plans, sets up automation frameworks, and ensures comprehensive test coverage.',
|
||||
expertise: [
|
||||
'Test strategy',
|
||||
'Test automation architecture',
|
||||
'Performance testing',
|
||||
'Integration testing',
|
||||
'CI/CD test integration',
|
||||
'Bug triage',
|
||||
],
|
||||
principles: [
|
||||
'Test early, test often',
|
||||
'Automate regression tests',
|
||||
'Focus on critical user journeys',
|
||||
'Quality is built in, not tested in',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
enterprise: {
|
||||
name: 'Enterprise Team',
|
||||
description: 'Full team for large/critical projects (10 agents)',
|
||||
roles: [
|
||||
{
|
||||
id: 'product-owner',
|
||||
name: 'Product Owner',
|
||||
icon: '📋',
|
||||
role: 'Product Strategy & Vision',
|
||||
identity:
|
||||
'Strategic product leader who defines the product vision, manages the backlog, and ensures business value delivery.',
|
||||
expertise: [
|
||||
'Product vision',
|
||||
'Roadmap planning',
|
||||
'Stakeholder management',
|
||||
'Market analysis',
|
||||
'Feature prioritization',
|
||||
'ROI analysis',
|
||||
],
|
||||
principles: [
|
||||
'Data-driven decisions',
|
||||
'User-centric design',
|
||||
'Clear success metrics',
|
||||
'Transparent prioritization',
|
||||
],
|
||||
tools: ['Perplexity for market research', 'Competitive analysis'],
|
||||
},
|
||||
{
|
||||
id: 'scrum-master',
|
||||
name: 'Scrum Master',
|
||||
icon: '🎯',
|
||||
role: 'Agile Coach & Process Facilitator',
|
||||
identity:
|
||||
'Process guardian who ensures the team follows agile practices, removes blockers, and facilitates ceremonies.',
|
||||
expertise: [
|
||||
'Agile methodologies',
|
||||
'Sprint planning',
|
||||
'Retrospectives',
|
||||
'Blocker resolution',
|
||||
'Team velocity tracking',
|
||||
'Process improvement',
|
||||
],
|
||||
principles: [
|
||||
'Servant leadership',
|
||||
'Continuous improvement',
|
||||
'Protect the team',
|
||||
'Foster collaboration',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'architect',
|
||||
name: 'Solutions Architect',
|
||||
icon: '🏗️',
|
||||
role: 'Enterprise Architecture Specialist',
|
||||
identity:
|
||||
'Senior architect who designs large-scale systems, ensures consistency across teams, and makes strategic technical decisions.',
|
||||
expertise: [
|
||||
'Enterprise architecture',
|
||||
'Microservices design',
|
||||
'Cloud architecture',
|
||||
'API gateway patterns',
|
||||
'Event-driven architecture',
|
||||
'Data architecture',
|
||||
],
|
||||
principles: [
|
||||
'Architect for the enterprise',
|
||||
'Standardize where it matters',
|
||||
'Document everything',
|
||||
'Plan for 10x scale',
|
||||
],
|
||||
tools: ['Perplexity for architecture patterns research'],
|
||||
},
|
||||
{
|
||||
id: 'tech-lead',
|
||||
name: 'Tech Lead',
|
||||
icon: '👨💻',
|
||||
role: 'Technical Team Lead',
|
||||
identity:
|
||||
'Hands-on technical leader who guides the development team, makes day-to-day technical decisions, and ensures code quality.',
|
||||
expertise: [
|
||||
'Technical leadership',
|
||||
'Code review',
|
||||
'Sprint technical planning',
|
||||
'Technical debt management',
|
||||
'Team mentoring',
|
||||
],
|
||||
principles: [
|
||||
'Lead by example',
|
||||
'Unblock the team',
|
||||
'Balance velocity and quality',
|
||||
'Grow team capabilities',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'senior-dev-1',
|
||||
name: 'Senior Developer (Backend)',
|
||||
icon: '⚙️',
|
||||
role: 'Senior Backend Engineer',
|
||||
identity:
|
||||
'Backend specialist focused on APIs, business logic, data processing, and system integrations.',
|
||||
expertise: [
|
||||
'API development',
|
||||
'Database optimization',
|
||||
'Business logic implementation',
|
||||
'Integration patterns',
|
||||
'Performance tuning',
|
||||
],
|
||||
principles: [
|
||||
'API-first design',
|
||||
'Optimize for maintainability',
|
||||
'Handle errors gracefully',
|
||||
'Log meaningfully',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'senior-dev-2',
|
||||
name: 'Senior Developer (Frontend)',
|
||||
icon: '🎨',
|
||||
role: 'Senior Frontend Engineer',
|
||||
identity:
|
||||
'Frontend specialist focused on user interfaces, state management, and user experience implementation.',
|
||||
expertise: [
|
||||
'UI architecture',
|
||||
'State management',
|
||||
'Component design',
|
||||
'Accessibility',
|
||||
'Performance optimization',
|
||||
],
|
||||
principles: [
|
||||
'User experience first',
|
||||
'Accessible by default',
|
||||
'Responsive design',
|
||||
'Optimize perceived performance',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
name: 'Security Engineer',
|
||||
icon: '🔒',
|
||||
role: 'Application Security Engineer',
|
||||
identity:
|
||||
'Security specialist who performs security reviews, penetration testing mindset, and ensures compliance.',
|
||||
expertise: [
|
||||
'Security architecture review',
|
||||
'Threat modeling',
|
||||
'OWASP compliance',
|
||||
'Security testing',
|
||||
'Incident response',
|
||||
'Compliance (SOC2, GDPR)',
|
||||
],
|
||||
principles: [
|
||||
'Shift security left',
|
||||
'Zero trust architecture',
|
||||
'Defense in depth',
|
||||
'Security by design',
|
||||
],
|
||||
tools: ['Perplexity for CVE and security research'],
|
||||
},
|
||||
{
|
||||
id: 'devops',
|
||||
name: 'DevOps Engineer',
|
||||
icon: '🚀',
|
||||
role: 'DevOps & Platform Engineer',
|
||||
identity:
|
||||
'Infrastructure and automation specialist who manages CI/CD, deployments, and platform reliability.',
|
||||
expertise: [
|
||||
'CI/CD pipelines',
|
||||
'Infrastructure as Code',
|
||||
'Container orchestration',
|
||||
'Monitoring & alerting',
|
||||
'Deployment strategies',
|
||||
'Cost optimization',
|
||||
],
|
||||
principles: [
|
||||
'Automate everything',
|
||||
'Infrastructure as code',
|
||||
'Monitor proactively',
|
||||
'Plan for failure',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'qa-lead',
|
||||
name: 'QA Lead',
|
||||
icon: '🧪',
|
||||
role: 'Quality Engineering Lead',
|
||||
identity:
|
||||
'Quality strategist who defines test strategy, manages QA team, and ensures comprehensive quality coverage.',
|
||||
expertise: [
|
||||
'Test strategy',
|
||||
'Automation frameworks',
|
||||
'Performance testing',
|
||||
'Security testing',
|
||||
'Quality metrics',
|
||||
'Release quality gates',
|
||||
],
|
||||
principles: [
|
||||
'Quality is everyones job',
|
||||
'Shift testing left',
|
||||
'Automate strategically',
|
||||
'Measure quality',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tech-writer',
|
||||
name: 'Technical Writer',
|
||||
icon: '📝',
|
||||
role: 'Documentation Specialist',
|
||||
identity:
|
||||
'Documentation expert who creates clear, comprehensive technical documentation for developers and users.',
|
||||
expertise: [
|
||||
'API documentation',
|
||||
'User guides',
|
||||
'Architecture documentation',
|
||||
'README files',
|
||||
'Runbooks',
|
||||
'Knowledge base',
|
||||
],
|
||||
principles: [
|
||||
'Write for the audience',
|
||||
'Keep docs up to date',
|
||||
'Include examples',
|
||||
'Document decisions',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export async function teamCommand(modulePath, options) {
|
||||
// List templates (no path required)
|
||||
if (options.list) {
|
||||
console.log(chalk.blue('\n📋 Available Team Templates:\n'));
|
||||
|
||||
for (const [key, template] of Object.entries(TEAM_TEMPLATES)) {
|
||||
console.log(chalk.cyan(` ${key}`));
|
||||
console.log(chalk.white(` ${template.name} - ${template.description}`));
|
||||
console.log(chalk.gray(` Roles: ${template.roles.map((r) => r.name).join(', ')}`));
|
||||
console.log('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Require path if not listing
|
||||
if (!modulePath) {
|
||||
console.log(chalk.red('Error: Path is required'));
|
||||
console.log(chalk.gray('Usage: npx agent-knowledge team <path> [options]'));
|
||||
console.log(chalk.gray(' npx agent-knowledge team --list'));
|
||||
return;
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
const fullPath = path.join(cwd, modulePath);
|
||||
const template = TEAM_TEMPLATES[options.template];
|
||||
|
||||
if (!template) {
|
||||
console.log(chalk.red(`Unknown template: ${options.template}`));
|
||||
console.log(chalk.gray('Use --list to see available templates'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`\n👥 Adding ${template.name} to ${modulePath}\n`));
|
||||
console.log(chalk.gray(`Template: ${template.description}\n`));
|
||||
|
||||
// Validate path
|
||||
if (!(await fs.pathExists(fullPath))) {
|
||||
console.log(chalk.red(`Path does not exist: ${fullPath}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create .knowledge directory if needed (minimal setup for Claude Code only mode)
|
||||
const knowledgeDir = path.join(cwd, '.knowledge');
|
||||
if (!(await fs.pathExists(knowledgeDir))) {
|
||||
await fs.ensureDir(knowledgeDir);
|
||||
// Create minimal registry
|
||||
await fs.writeFile(path.join(knowledgeDir, 'agent-registry.yaml'), 'agents: []\n');
|
||||
console.log(chalk.gray('Created .knowledge/ directory (Claude Code only mode)\n'));
|
||||
}
|
||||
|
||||
// Show team preview
|
||||
console.log(chalk.cyan('Team members to create:\n'));
|
||||
for (const role of template.roles) {
|
||||
console.log(` ${role.icon} ${chalk.white(role.name)} - ${chalk.gray(role.role)}`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log(chalk.yellow('[DRY RUN] No files created'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `Create ${template.roles.length} team agents?`,
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!confirm) {
|
||||
console.log(chalk.yellow('Aborted.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ora('Scanning module for context...').start();
|
||||
|
||||
// Gather domain knowledge from the module
|
||||
const domainContext = await gatherDomainContext(fullPath, modulePath);
|
||||
|
||||
spinner.text = 'Creating team...';
|
||||
|
||||
try {
|
||||
// Create .agent/team directory
|
||||
const teamDir = path.join(fullPath, '.agent', 'team');
|
||||
await fs.ensureDir(teamDir);
|
||||
|
||||
// Get module name for context
|
||||
const moduleName = path.basename(modulePath);
|
||||
|
||||
// Create each team member with domain context
|
||||
for (const role of template.roles) {
|
||||
const agentFile = path.join(teamDir, `${role.id}.md`);
|
||||
|
||||
const content = generateTeamAgentContent(role, moduleName, modulePath, domainContext);
|
||||
await fs.writeFile(agentFile, content);
|
||||
|
||||
spinner.text = `Created ${role.name}...`;
|
||||
}
|
||||
|
||||
// Create team index
|
||||
const indexContent = generateTeamIndex(template, moduleName, modulePath);
|
||||
await fs.writeFile(path.join(fullPath, '.agent', 'team.md'), indexContent);
|
||||
|
||||
// Create/update main agent.md if it doesn't exist
|
||||
const mainAgentPath = path.join(fullPath, '.agent', 'agent.md');
|
||||
if (!(await fs.pathExists(mainAgentPath))) {
|
||||
const mainContent = generateMainAgentWithTeam(template, moduleName, modulePath);
|
||||
await fs.writeFile(mainAgentPath, mainContent);
|
||||
}
|
||||
|
||||
// Create memory.md if it doesn't exist
|
||||
const memoryPath = path.join(fullPath, '.agent', 'memory.md');
|
||||
if (!(await fs.pathExists(memoryPath))) {
|
||||
await fs.writeFile(
|
||||
memoryPath,
|
||||
`# ${moduleName} - Memory
|
||||
|
||||
This file is automatically updated with learnings from code changes.
|
||||
|
||||
## Recent Updates
|
||||
|
||||
*No updates yet.*
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
// Update registry with team watch patterns
|
||||
await updateRegistryWithTeam(cwd, modulePath, template);
|
||||
|
||||
spinner.succeed(`Created ${template.roles.length} team agents`);
|
||||
|
||||
console.log(chalk.green(`\n✅ Team created at ${modulePath}/.agent/team/\n`));
|
||||
|
||||
console.log(chalk.cyan('Files created:'));
|
||||
console.log(chalk.gray(` ${modulePath}/.agent/team.md - Team overview`));
|
||||
for (const role of template.roles) {
|
||||
console.log(chalk.gray(` ${modulePath}/.agent/team/${role.id}.md`));
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('\nUsage with Claude:'));
|
||||
console.log(chalk.white(` "Read ${modulePath}/.agent/team.md and help as the Architect"`));
|
||||
console.log(chalk.white(` "Act as the Security Engineer and review this code"`));
|
||||
console.log(chalk.white(` "As the Product Owner, help me write user stories"\n`));
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to create team');
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather domain-specific context from the module
|
||||
*/
|
||||
async function gatherDomainContext(fullPath, modulePath) {
|
||||
const context = {
|
||||
name: path.basename(modulePath),
|
||||
description: '',
|
||||
techStack: [],
|
||||
features: [],
|
||||
structure: [],
|
||||
dependencies: [],
|
||||
readme: '',
|
||||
hasBackend: false,
|
||||
hasMobile: false,
|
||||
hasWeb: false,
|
||||
hasLanding: false,
|
||||
};
|
||||
|
||||
// Check for package.json
|
||||
const packageJsonPath = path.join(fullPath, 'package.json');
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
try {
|
||||
const pkg = await fs.readJson(packageJsonPath);
|
||||
context.description = pkg.description || '';
|
||||
context.dependencies = Object.keys(pkg.dependencies || {}).slice(0, 15);
|
||||
|
||||
// Detect tech from dependencies
|
||||
if (pkg.dependencies?.['@nestjs/core']) context.techStack.push('NestJS');
|
||||
if (pkg.dependencies?.['svelte'] || pkg.dependencies?.['@sveltejs/kit'])
|
||||
context.techStack.push('SvelteKit');
|
||||
if (pkg.dependencies?.['expo'] || pkg.dependencies?.['react-native'])
|
||||
context.techStack.push('Expo/React Native');
|
||||
if (pkg.dependencies?.['astro']) context.techStack.push('Astro');
|
||||
if (pkg.dependencies?.['next']) context.techStack.push('Next.js');
|
||||
if (pkg.dependencies?.['drizzle-orm']) context.techStack.push('Drizzle ORM');
|
||||
if (pkg.dependencies?.['prisma']) context.techStack.push('Prisma');
|
||||
if (pkg.dependencies?.['@supabase/supabase-js']) context.techStack.push('Supabase');
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
// Check for README
|
||||
for (const readmeName of ['README.md', 'readme.md']) {
|
||||
const readmePath = path.join(fullPath, readmeName);
|
||||
if (await fs.pathExists(readmePath)) {
|
||||
const content = await fs.readFile(readmePath, 'utf-8');
|
||||
context.readme = content.substring(0, 2000);
|
||||
|
||||
// Try to extract features from README
|
||||
const featuresMatch = content.match(/## Features[\s\S]*?(?=##|$)/i);
|
||||
if (featuresMatch) {
|
||||
const featureLines = featuresMatch[0].match(/^[-*]\s+.+$/gm);
|
||||
if (featureLines) {
|
||||
context.features = featureLines.slice(0, 10).map((f) => f.replace(/^[-*]\s+/, ''));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for nested app structure (like ManaCore)
|
||||
const appsDir = path.join(fullPath, 'apps');
|
||||
if (await fs.pathExists(appsDir)) {
|
||||
const subApps = await fs.readdir(appsDir).catch(() => []);
|
||||
context.hasBackend = subApps.includes('backend');
|
||||
context.hasMobile = subApps.includes('mobile');
|
||||
context.hasWeb = subApps.includes('web');
|
||||
context.hasLanding = subApps.includes('landing');
|
||||
|
||||
// Scan each sub-app for tech stack
|
||||
for (const subApp of subApps) {
|
||||
const subPkgPath = path.join(appsDir, subApp, 'package.json');
|
||||
if (await fs.pathExists(subPkgPath)) {
|
||||
try {
|
||||
const subPkg = await fs.readJson(subPkgPath);
|
||||
if (subPkg.dependencies?.['@nestjs/core'] && !context.techStack.includes('NestJS')) {
|
||||
context.techStack.push('NestJS');
|
||||
}
|
||||
if (
|
||||
(subPkg.dependencies?.['svelte'] || subPkg.dependencies?.['@sveltejs/kit']) &&
|
||||
!context.techStack.includes('SvelteKit')
|
||||
) {
|
||||
context.techStack.push('SvelteKit');
|
||||
}
|
||||
if (
|
||||
(subPkg.dependencies?.['expo'] || subPkg.dependencies?.['react-native']) &&
|
||||
!context.techStack.includes('Expo/React Native')
|
||||
) {
|
||||
context.techStack.push('Expo/React Native');
|
||||
}
|
||||
if (subPkg.dependencies?.['astro'] && !context.techStack.includes('Astro')) {
|
||||
context.techStack.push('Astro');
|
||||
}
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan src directory for structure hints
|
||||
const srcDir = path.join(fullPath, 'src');
|
||||
if (await fs.pathExists(srcDir)) {
|
||||
const srcContents = await fs.readdir(srcDir).catch(() => []);
|
||||
context.structure = srcContents.filter((f) => !f.startsWith('.')).slice(0, 10);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function generateTeamAgentContent(role, moduleName, modulePath, domainContext = {}) {
|
||||
const expertiseList = role.expertise.map((e) => `- ${e}`).join('\n');
|
||||
const principlesList = role.principles.map((p, i) => `${i + 1}. ${p}`).join('\n');
|
||||
const toolsList = role.tools ? `\n## Tools\n${role.tools.map((t) => `- ${t}`).join('\n')}\n` : '';
|
||||
|
||||
// Build domain-specific sections
|
||||
const domainSection = generateDomainSection(role, moduleName, domainContext);
|
||||
|
||||
return `# ${role.icon} ${role.name}
|
||||
|
||||
## Module Context
|
||||
**Module:** ${moduleName}
|
||||
**Path:** ${modulePath}
|
||||
${domainContext.description ? `**Description:** ${domainContext.description}` : ''}
|
||||
${domainContext.techStack?.length > 0 ? `**Tech Stack:** ${domainContext.techStack.join(', ')}` : ''}
|
||||
${domainContext.hasBackend || domainContext.hasMobile || domainContext.hasWeb ? `**Platforms:** ${[domainContext.hasBackend && 'Backend', domainContext.hasMobile && 'Mobile', domainContext.hasWeb && 'Web', domainContext.hasLanding && 'Landing'].filter(Boolean).join(', ')}` : ''}
|
||||
|
||||
## Identity
|
||||
${role.identity}
|
||||
|
||||
## Role
|
||||
${role.role}
|
||||
${domainSection}
|
||||
## Expertise
|
||||
${expertiseList}
|
||||
|
||||
## Principles
|
||||
${principlesList}
|
||||
${toolsList}
|
||||
## How to Invoke
|
||||
Ask Claude to act as this role:
|
||||
- "As the ${role.name}, review this code"
|
||||
- "Help me as the ${role.name} with..."
|
||||
- "What would the ${role.name} think about..."
|
||||
|
||||
## Collaboration
|
||||
This agent works with other team members. For complex tasks:
|
||||
- Consult the **Architect** for system design decisions
|
||||
- Coordinate with **Security** for security-sensitive changes
|
||||
- Work with **QA** on test coverage
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate domain-specific content based on role and context
|
||||
*/
|
||||
function generateDomainSection(role, moduleName, context) {
|
||||
if (!context || Object.keys(context).length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sections = [];
|
||||
|
||||
// Product Owner - needs to know about features and product
|
||||
if (role.id === 'product-owner') {
|
||||
sections.push(`
|
||||
## Product Knowledge: ${moduleName}
|
||||
`);
|
||||
if (context.description) {
|
||||
sections.push(`**What it does:** ${context.description}`);
|
||||
}
|
||||
if (context.features?.length > 0) {
|
||||
sections.push(`
|
||||
**Known Features:**
|
||||
${context.features.map((f) => `- ${f}`).join('\n')}`);
|
||||
}
|
||||
if (context.hasBackend || context.hasMobile || context.hasWeb) {
|
||||
sections.push(`
|
||||
**Available Platforms:**
|
||||
${context.hasBackend ? '- Backend API (NestJS)\n' : ''}${context.hasMobile ? '- Mobile App (Expo/React Native)\n' : ''}${context.hasWeb ? '- Web App (SvelteKit)\n' : ''}${context.hasLanding ? '- Landing Page (Astro)\n' : ''}`);
|
||||
}
|
||||
sections.push(`
|
||||
**Your Focus:** Understand what users need from ${moduleName}, define clear requirements, and prioritize features that deliver the most value.`);
|
||||
}
|
||||
|
||||
// Architect - needs to know about tech stack and structure
|
||||
if (role.id === 'architect' || role.id === 'tech-lead') {
|
||||
sections.push(`
|
||||
## Technical Context: ${moduleName}
|
||||
`);
|
||||
if (context.techStack?.length > 0) {
|
||||
sections.push(`**Tech Stack:** ${context.techStack.join(', ')}`);
|
||||
}
|
||||
if (context.structure?.length > 0) {
|
||||
sections.push(`
|
||||
**Code Structure:**
|
||||
\`\`\`
|
||||
src/
|
||||
${context.structure.map((s) => `├── ${s}/`).join('\n')}
|
||||
\`\`\``);
|
||||
}
|
||||
if (context.dependencies?.length > 0) {
|
||||
sections.push(`
|
||||
**Key Dependencies:** ${context.dependencies.slice(0, 8).join(', ')}`);
|
||||
}
|
||||
if (context.hasBackend || context.hasMobile || context.hasWeb) {
|
||||
sections.push(`
|
||||
**Architecture Pattern:** Multi-platform app with ${[context.hasBackend && 'Backend', context.hasMobile && 'Mobile', context.hasWeb && 'Web'].filter(Boolean).join(' + ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Backend devs - need backend context
|
||||
if (role.id === 'senior-dev-1' || (role.id === 'senior-dev' && context.hasBackend)) {
|
||||
if (context.hasBackend) {
|
||||
sections.push(`
|
||||
## Backend Context
|
||||
**Framework:** NestJS
|
||||
**Location:** \`${context.name}/apps/backend/\`
|
||||
**Focus:** API endpoints, business logic, database operations`);
|
||||
}
|
||||
}
|
||||
|
||||
// Frontend devs - need frontend context
|
||||
if (role.id === 'senior-dev-2' || role.id === 'developer') {
|
||||
if (context.hasWeb || context.hasMobile) {
|
||||
sections.push(`
|
||||
## Frontend Context
|
||||
${context.hasWeb ? `**Web:** SvelteKit at \`${context.name}/apps/web/\`\n` : ''}${context.hasMobile ? `**Mobile:** Expo at \`${context.name}/apps/mobile/\`\n` : ''}**Focus:** User interface, state management, user experience`);
|
||||
}
|
||||
}
|
||||
|
||||
// Security - needs to know about auth and sensitive areas
|
||||
if (role.id === 'security') {
|
||||
sections.push(`
|
||||
## Security Context: ${moduleName}
|
||||
**Review Focus:**
|
||||
- Authentication flows (if using @manacore/shared-auth)
|
||||
- API endpoint authorization
|
||||
- Input validation on all endpoints
|
||||
- Sensitive data handling
|
||||
- Dependency vulnerabilities
|
||||
|
||||
${context.hasBackend ? '**Backend Security:** Review NestJS guards, validators, and middleware\n' : ''}${context.hasMobile || context.hasWeb ? '**Client Security:** Review token storage, API calls, input sanitization\n' : ''}`);
|
||||
}
|
||||
|
||||
// QA - needs to know about testable features
|
||||
if (role.id === 'qa-lead' || role.id === 'qa') {
|
||||
sections.push(`
|
||||
## Testing Context: ${moduleName}
|
||||
`);
|
||||
if (context.features?.length > 0) {
|
||||
sections.push(`**Features to Test:**
|
||||
${context.features
|
||||
.slice(0, 5)
|
||||
.map((f) => `- [ ] ${f}`)
|
||||
.join('\n')}`);
|
||||
}
|
||||
if (context.hasBackend || context.hasMobile || context.hasWeb) {
|
||||
sections.push(`
|
||||
**Test Coverage Needed:**
|
||||
${context.hasBackend ? '- [ ] API endpoint tests (backend)\n' : ''}${context.hasWeb ? '- [ ] E2E tests (web)\n' : ''}${context.hasMobile ? '- [ ] Mobile app tests\n' : ''}${context.hasLanding ? '- [ ] Landing page tests\n' : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
// DevOps - needs deployment context
|
||||
if (role.id === 'devops') {
|
||||
sections.push(`
|
||||
## DevOps Context: ${moduleName}
|
||||
**Deployments:**
|
||||
${context.hasBackend ? '- Backend: NestJS service\n' : ''}${context.hasWeb ? '- Web: SvelteKit (SSR or static)\n' : ''}${context.hasMobile ? '- Mobile: Expo (iOS/Android builds)\n' : ''}${context.hasLanding ? '- Landing: Astro (static)\n' : ''}
|
||||
**Focus:** CI/CD pipelines, environment configuration, monitoring`);
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
function generateTeamIndex(template, moduleName, modulePath) {
|
||||
let content = `# ${moduleName} Development Team
|
||||
|
||||
## Team Template: ${template.name}
|
||||
|
||||
${template.description}
|
||||
|
||||
## Module Context
|
||||
- **Module:** ${moduleName}
|
||||
- **Path:** ${modulePath}
|
||||
|
||||
## Team Members
|
||||
|
||||
| Role | Specialty | File |
|
||||
|------|-----------|------|
|
||||
`;
|
||||
|
||||
for (const role of template.roles) {
|
||||
content += `| ${role.icon} ${role.name} | ${role.role} | [${role.id}.md](./team/${role.id}.md) |\n`;
|
||||
}
|
||||
|
||||
content += `
|
||||
## How to Use This Team
|
||||
|
||||
### Single Role
|
||||
Ask Claude to act as a specific team member:
|
||||
\`\`\`
|
||||
Read ${modulePath}/.agent/team/architect.md and help me design the API
|
||||
\`\`\`
|
||||
|
||||
### Multiple Perspectives
|
||||
Get input from multiple team members:
|
||||
\`\`\`
|
||||
Read the team files in ${modulePath}/.agent/team/ and give me perspectives
|
||||
from the Architect, Security Engineer, and QA Lead on this approach
|
||||
\`\`\`
|
||||
|
||||
### Team Review
|
||||
Request a full team review:
|
||||
\`\`\`
|
||||
Act as each team member in ${modulePath}/.agent/team/ and review this PR
|
||||
\`\`\`
|
||||
|
||||
## Team Dynamics
|
||||
|
||||
### For New Features
|
||||
1. **Product Owner** - Define requirements and acceptance criteria
|
||||
2. **Architect** - Design the solution
|
||||
3. **Senior Developer** - Implement with code review
|
||||
4. **Security** - Security review
|
||||
5. **QA** - Test planning and execution
|
||||
|
||||
### For Bug Fixes
|
||||
1. **Developer** - Investigate and fix
|
||||
2. **QA** - Verify fix and check for regression
|
||||
3. **Security** - If security-related, review the fix
|
||||
|
||||
### For Architecture Changes
|
||||
1. **Architect** - Propose and document design
|
||||
2. **Tech Lead** - Review feasibility
|
||||
3. **Security** - Assess security implications
|
||||
4. **DevOps** - Assess operational impact
|
||||
`;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function generateMainAgentWithTeam(template, moduleName, modulePath) {
|
||||
return `# ${moduleName} Expert
|
||||
|
||||
## Identity
|
||||
You are the **${moduleName} Expert** with access to a full development team.
|
||||
You can channel any team member's expertise as needed.
|
||||
|
||||
## Team Available
|
||||
${template.roles.map((r) => `- ${r.icon} **${r.name}** - ${r.role}`).join('\n')}
|
||||
|
||||
See [team.md](./team.md) for full team details.
|
||||
|
||||
## How to Use
|
||||
- For general questions, I'll respond as the module expert
|
||||
- For specific perspectives, ask me to act as a team member
|
||||
- For comprehensive reviews, I can provide multiple perspectives
|
||||
|
||||
## Module Context
|
||||
- **Path:** ${modulePath}
|
||||
- **Team:** ${template.name} (${template.roles.length} roles)
|
||||
|
||||
## Principles
|
||||
1. Leverage the right expertise for each task
|
||||
2. Consider multiple perspectives on complex decisions
|
||||
3. Maintain knowledge continuity through memory.md
|
||||
`;
|
||||
}
|
||||
|
||||
async function updateRegistryWithTeam(cwd, modulePath, template) {
|
||||
const registryPath = path.join(cwd, '.knowledge', 'agent-registry.yaml');
|
||||
let registry = { agents: [] };
|
||||
|
||||
if (await fs.pathExists(registryPath)) {
|
||||
const content = await fs.readFile(registryPath, 'utf-8');
|
||||
registry = yaml.parse(content) || { agents: [] };
|
||||
}
|
||||
|
||||
// Remove existing entry
|
||||
registry.agents = registry.agents.filter((a) => a.path !== modulePath);
|
||||
|
||||
// Add new entry with team info
|
||||
registry.agents.push({
|
||||
path: modulePath,
|
||||
agent_dir: `${modulePath}/.agent`,
|
||||
name: `${path.basename(modulePath)} Team`,
|
||||
team: template.name,
|
||||
team_size: template.roles.length,
|
||||
watches: [`${modulePath}/**`],
|
||||
});
|
||||
|
||||
await fs.writeFile(registryPath, yaml.stringify(registry));
|
||||
}
|
||||
368
tools/agent-knowledge/src/commands/update.js
Normal file
368
tools/agent-knowledge/src/commands/update.js
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import yaml from 'yaml';
|
||||
import readline from 'readline';
|
||||
import { spawn } from 'child_process';
|
||||
import { LLMClient } from '../lib/llm-client.js';
|
||||
import { loadConfig } from '../lib/config.js';
|
||||
|
||||
export async function updateCommand(options) {
|
||||
const cwd = process.cwd();
|
||||
const knowledgeDir = path.join(cwd, '.knowledge');
|
||||
|
||||
console.log(chalk.blue('\n🔄 Updating Agent Knowledge...\n'));
|
||||
|
||||
// Check initialization
|
||||
if (!(await fs.pathExists(knowledgeDir))) {
|
||||
console.log(chalk.red('Error: Agent Knowledge System not initialized.'));
|
||||
console.log(chalk.gray('Run: npx agent-knowledge init'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Load registry
|
||||
const registryPath = path.join(knowledgeDir, 'agent-registry.yaml');
|
||||
const registry = yaml.parse(await fs.readFile(registryPath, 'utf-8'));
|
||||
|
||||
if (registry.agents.length === 0) {
|
||||
console.log(chalk.yellow('No agents registered. Add agents first:'));
|
||||
console.log(chalk.gray(' npx agent-knowledge add-agent <path>'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Read changes
|
||||
const changesPath = path.join(knowledgeDir, 'changes.jsonl');
|
||||
const changes = await readChanges(changesPath);
|
||||
|
||||
if (changes.length === 0) {
|
||||
console.log(chalk.yellow('No pending changes to process.'));
|
||||
console.log(chalk.gray('Make some commits and changes will be tracked automatically.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.gray(`Found ${changes.length} changes to process\n`));
|
||||
|
||||
// Filter agents if specific one requested
|
||||
let agents = registry.agents;
|
||||
if (options.agent) {
|
||||
agents = agents.filter((a) => a.path === options.agent || a.agent_dir.includes(options.agent));
|
||||
if (agents.length === 0) {
|
||||
console.log(chalk.red(`No agent found matching: ${options.agent}`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Group changes by agent
|
||||
const changesByAgent = groupChangesByAgent(changes, agents);
|
||||
|
||||
// If --claude mode, generate prompt file and spawn Claude Code
|
||||
if (options.claude) {
|
||||
await updateWithClaude(cwd, knowledgeDir, agents, changesByAgent, options);
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard LLM mode
|
||||
const config = await loadConfig(cwd);
|
||||
const llm = new LLMClient(config);
|
||||
|
||||
if (options.model) {
|
||||
console.log(chalk.gray(`Using model override: ${options.model}\n`));
|
||||
}
|
||||
|
||||
// Process each agent
|
||||
let totalUpdated = 0;
|
||||
for (const agent of agents) {
|
||||
const agentChanges = changesByAgent[agent.agent_dir] || [];
|
||||
|
||||
if (agentChanges.length === 0) {
|
||||
console.log(chalk.gray(`⏭️ ${agent.name}: No relevant changes`));
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\n🤖 ${agent.name} (${agentChanges.length} changes)`));
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log(chalk.gray(' [DRY RUN] Would update memory.md with:'));
|
||||
for (const change of agentChanges.slice(0, 5)) {
|
||||
console.log(chalk.gray(` - ${change.file}: ${change.message.substring(0, 50)}`));
|
||||
}
|
||||
if (agentChanges.length > 5) {
|
||||
console.log(chalk.gray(` ... and ${agentChanges.length - 5} more`));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const spinner = ora('Updating memory...').start();
|
||||
|
||||
try {
|
||||
await updateAgentMemory(cwd, agent, agentChanges, llm, options.model);
|
||||
spinner.succeed(`Updated ${agent.agent_dir}/memory.md`);
|
||||
totalUpdated++;
|
||||
} catch (error) {
|
||||
spinner.fail(`Failed to update ${agent.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Archive processed changes (unless dry run)
|
||||
if (!options.dryRun && totalUpdated > 0) {
|
||||
await archiveChanges(knowledgeDir);
|
||||
console.log(chalk.gray('\n📦 Changes archived'));
|
||||
}
|
||||
|
||||
console.log(chalk.green(`\n✅ Updated ${totalUpdated} agent(s)\n`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update agent memories using Claude Code
|
||||
*/
|
||||
async function updateWithClaude(cwd, knowledgeDir, agents, changesByAgent, options) {
|
||||
// Filter to only agents with changes
|
||||
const agentsWithChanges = agents.filter((a) => (changesByAgent[a.agent_dir] || []).length > 0);
|
||||
|
||||
if (agentsWithChanges.length === 0) {
|
||||
console.log(chalk.yellow('No agents have relevant changes.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.cyan(`\n📝 Generating Claude Code prompt for ${agentsWithChanges.length} agent(s)...\n`)
|
||||
);
|
||||
|
||||
// Build the prompt
|
||||
let prompt = `# Agent Memory Update Task
|
||||
|
||||
You need to update the memory.md files for the following agents based on recent code changes.
|
||||
|
||||
## Instructions
|
||||
For EACH agent listed below:
|
||||
1. Read the agent's existing memory.md file (if it exists)
|
||||
2. Analyze the changes relevant to that agent
|
||||
3. Update the memory.md with new learnings:
|
||||
- Add a dated section at the TOP (under "## Recent Updates")
|
||||
- Extract: new patterns, bug fixes, breaking changes, new features, architectural decisions
|
||||
- Be concise but capture important context
|
||||
- Keep under 400 lines total
|
||||
4. Write the updated content to the memory.md file
|
||||
|
||||
## Agents to Update
|
||||
|
||||
`;
|
||||
|
||||
for (const agent of agentsWithChanges) {
|
||||
const agentChanges = changesByAgent[agent.agent_dir] || [];
|
||||
|
||||
// Group by date
|
||||
const byDate = {};
|
||||
for (const change of agentChanges) {
|
||||
const date = change.timestamp?.split('T')[0] || 'unknown';
|
||||
byDate[date] = byDate[date] || [];
|
||||
byDate[date].push({
|
||||
file: change.file,
|
||||
message: change.message,
|
||||
author: change.author,
|
||||
status: change.status,
|
||||
});
|
||||
}
|
||||
|
||||
prompt += `### ${agent.name}
|
||||
- **Memory file**: \`${agent.agent_dir}/memory.md\`
|
||||
- **Changes**: ${agentChanges.length} file(s) modified
|
||||
|
||||
\`\`\`json
|
||||
${JSON.stringify(byDate, null, 2)}
|
||||
\`\`\`
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
prompt += `
|
||||
## After Completion
|
||||
After updating all memory.md files, provide a summary of what was updated.
|
||||
`;
|
||||
|
||||
// Write prompt to file
|
||||
const promptPath = path.join(knowledgeDir, 'claude-update-memory.md');
|
||||
await fs.writeFile(promptPath, prompt);
|
||||
console.log(chalk.gray(`Prompt written to: ${promptPath}\n`));
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log(chalk.yellow('[DRY RUN] Would run Claude Code with the above prompt.'));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`\nTo run manually:\n cd ${cwd} && claude --dangerously-skip-permissions "Read .knowledge/claude-update-memory.md and update all agent memory files."\n`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask user if they want to run Claude now
|
||||
const inquirer = (await import('inquirer')).default;
|
||||
const { autoRun } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'autoRun',
|
||||
message: 'Run Claude Code now to update memory files?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (autoRun) {
|
||||
console.log(chalk.cyan('\n🚀 Spawning Claude Code...\n'));
|
||||
|
||||
const claudePrompt =
|
||||
'Read .knowledge/claude-update-memory.md and update all agent memory files.';
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const claude = spawn('claude', ['--dangerously-skip-permissions', claudePrompt], {
|
||||
cwd,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
claude.on('error', (err) => {
|
||||
console.error(chalk.red(`Failed to spawn Claude Code: ${err.message}`));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`\nRun manually:\n cd ${cwd} && claude --dangerously-skip-permissions "${claudePrompt}"\n`
|
||||
)
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
|
||||
claude.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log(chalk.green('\n✅ Claude Code completed successfully'));
|
||||
// Archive changes after successful update
|
||||
archiveChanges(knowledgeDir).then(() => {
|
||||
console.log(chalk.gray('📦 Changes archived'));
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.yellow(`\nClaude Code exited with code ${code}`));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`\nRun manually when ready:\n cd ${cwd} && claude --dangerously-skip-permissions "Read .knowledge/claude-update-memory.md and update all agent memory files."\n`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function readChanges(changesPath) {
|
||||
const changes = [];
|
||||
|
||||
if (!(await fs.pathExists(changesPath))) {
|
||||
return changes;
|
||||
}
|
||||
|
||||
const content = await fs.readFile(changesPath, 'utf-8');
|
||||
for (const line of content.split('\n')) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
changes.push(JSON.parse(line));
|
||||
} catch (e) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function groupChangesByAgent(changes, agents) {
|
||||
const grouped = {};
|
||||
|
||||
for (const agent of agents) {
|
||||
grouped[agent.agent_dir] = [];
|
||||
|
||||
for (const change of changes) {
|
||||
const matches = agent.watches.some((pattern) => {
|
||||
const regex = new RegExp(
|
||||
'^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*').replace(/\//g, '\\/') + '$'
|
||||
);
|
||||
return regex.test(change.file);
|
||||
});
|
||||
|
||||
if (matches) {
|
||||
grouped[agent.agent_dir].push(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
async function updateAgentMemory(cwd, agent, changes, llm, modelOverride) {
|
||||
const memoryPath = path.join(cwd, agent.agent_dir, 'memory.md');
|
||||
const existingMemory = (await fs.pathExists(memoryPath))
|
||||
? await fs.readFile(memoryPath, 'utf-8')
|
||||
: '';
|
||||
|
||||
// Group changes by date
|
||||
const byDate = {};
|
||||
for (const change of changes) {
|
||||
const date = change.timestamp?.split('T')[0] || 'unknown';
|
||||
byDate[date] = byDate[date] || [];
|
||||
byDate[date].push({
|
||||
file: change.file,
|
||||
message: change.message,
|
||||
author: change.author,
|
||||
status: change.status,
|
||||
diff: change.diff?.substring(0, 500),
|
||||
});
|
||||
}
|
||||
|
||||
const prompt = `You are updating the memory/knowledge file for a specialized code agent called "${agent.name}".
|
||||
|
||||
## Existing Memory
|
||||
${existingMemory || '(empty - this is a new agent)'}
|
||||
|
||||
## Recent Changes to Process
|
||||
${JSON.stringify(byDate, null, 2)}
|
||||
|
||||
## Instructions
|
||||
1. Analyze the changes and extract important knowledge:
|
||||
- New patterns or approaches introduced
|
||||
- Breaking changes or deprecations
|
||||
- Bug fixes and why they were needed
|
||||
- New features or capabilities
|
||||
- Architectural decisions evident from the changes
|
||||
|
||||
2. Update the memory file:
|
||||
- Add a new dated section at the TOP (under "## Recent Updates")
|
||||
- Be concise but capture important context
|
||||
- Keep important historical context (don't delete valuable old entries)
|
||||
- Remove redundant or outdated information
|
||||
- Keep total length reasonable (under 400 lines)
|
||||
|
||||
3. Format as clean markdown with clear sections
|
||||
|
||||
Output ONLY the updated memory.md content, no explanations or code blocks.`;
|
||||
|
||||
const updatedMemory = await llm.complete(prompt, {
|
||||
model: modelOverride,
|
||||
maxTokens: 3000,
|
||||
});
|
||||
|
||||
await fs.writeFile(memoryPath, updatedMemory);
|
||||
}
|
||||
|
||||
async function archiveChanges(knowledgeDir) {
|
||||
const changesPath = path.join(knowledgeDir, 'changes.jsonl');
|
||||
const archiveDir = path.join(knowledgeDir, 'archive');
|
||||
|
||||
await fs.ensureDir(archiveDir);
|
||||
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
const archivePath = path.join(archiveDir, `changes-${date}.jsonl`);
|
||||
|
||||
// Append to today's archive
|
||||
const content = await fs.readFile(changesPath, 'utf-8');
|
||||
await fs.appendFile(archivePath, content);
|
||||
|
||||
// Clear main changes file
|
||||
await fs.writeFile(changesPath, '');
|
||||
}
|
||||
5
tools/agent-knowledge/src/index.js
Normal file
5
tools/agent-knowledge/src/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Agent Knowledge System
|
||||
// Main entry point for programmatic usage
|
||||
|
||||
export { LLMClient } from './lib/llm-client.js';
|
||||
export { loadConfig, saveConfig } from './lib/config.js';
|
||||
108
tools/agent-knowledge/src/lib/config.js
Normal file
108
tools/agent-knowledge/src/lib/config.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import yaml from 'yaml';
|
||||
|
||||
/**
|
||||
* Load configuration from .knowledge/config.yaml and .env
|
||||
*/
|
||||
export async function loadConfig(cwd) {
|
||||
const config = {
|
||||
provider: 'openrouter',
|
||||
model: 'google/gemini-flash-1.5',
|
||||
models: {
|
||||
batch: { provider: 'openrouter', model: 'google/gemini-flash-1.5' },
|
||||
quality: { provider: 'openrouter', model: 'anthropic/claude-sonnet-4-20250514' },
|
||||
fallbacks: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Load from config.yaml
|
||||
const configPath = path.join(cwd, '.knowledge', 'config.yaml');
|
||||
if (await fs.pathExists(configPath)) {
|
||||
const yamlConfig = yaml.parse(await fs.readFile(configPath, 'utf-8'));
|
||||
Object.assign(config, yamlConfig);
|
||||
}
|
||||
|
||||
// Load from .env
|
||||
const envPath = path.join(cwd, '.knowledge', '.env');
|
||||
if (await fs.pathExists(envPath)) {
|
||||
const envContent = await fs.readFile(envPath, 'utf-8');
|
||||
const envVars = parseEnv(envContent);
|
||||
|
||||
if (envVars.OPENROUTER_API_KEY) {
|
||||
config.openrouterApiKey = envVars.OPENROUTER_API_KEY;
|
||||
}
|
||||
if (envVars.ANTHROPIC_API_KEY) {
|
||||
config.anthropicApiKey = envVars.ANTHROPIC_API_KEY;
|
||||
}
|
||||
if (envVars.LLM_PROVIDER) {
|
||||
config.provider = envVars.LLM_PROVIDER;
|
||||
}
|
||||
if (envVars.OPENROUTER_MODEL) {
|
||||
config.model = envVars.OPENROUTER_MODEL;
|
||||
}
|
||||
if (envVars.ANTHROPIC_MODEL) {
|
||||
config.model = envVars.ANTHROPIC_MODEL;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check process environment (takes precedence)
|
||||
if (process.env.OPENROUTER_API_KEY) {
|
||||
config.openrouterApiKey = process.env.OPENROUTER_API_KEY;
|
||||
}
|
||||
if (process.env.ANTHROPIC_API_KEY) {
|
||||
config.anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
||||
}
|
||||
if (process.env.LLM_PROVIDER) {
|
||||
config.provider = process.env.LLM_PROVIDER;
|
||||
}
|
||||
if (process.env.OPENROUTER_MODEL || process.env.ANTHROPIC_MODEL) {
|
||||
config.model = process.env.OPENROUTER_MODEL || process.env.ANTHROPIC_MODEL;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple .env parser
|
||||
*/
|
||||
function parseEnv(content) {
|
||||
const vars = {};
|
||||
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Skip comments and empty lines
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const eqIndex = trimmed.indexOf('=');
|
||||
if (eqIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = trimmed.slice(0, eqIndex).trim();
|
||||
let value = trimmed.slice(eqIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
vars[key] = value;
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration
|
||||
*/
|
||||
export async function saveConfig(cwd, config) {
|
||||
const configPath = path.join(cwd, '.knowledge', 'config.yaml');
|
||||
await fs.writeFile(configPath, yaml.stringify(config));
|
||||
}
|
||||
160
tools/agent-knowledge/src/lib/llm-client.js
Normal file
160
tools/agent-knowledge/src/lib/llm-client.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import chalk from 'chalk';
|
||||
|
||||
/**
|
||||
* Unified LLM client supporting multiple providers
|
||||
*/
|
||||
export class LLMClient {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.provider = config.provider || 'openrouter';
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a prompt using the configured provider
|
||||
*/
|
||||
async complete(prompt, options = {}) {
|
||||
const provider = options.provider || this.provider;
|
||||
|
||||
try {
|
||||
if (provider === 'anthropic') {
|
||||
return await this.callAnthropic(prompt, options);
|
||||
} else if (provider === 'openrouter') {
|
||||
return await this.callOpenRouter(prompt, options);
|
||||
}
|
||||
throw new Error(`Unknown provider: ${provider}`);
|
||||
} catch (error) {
|
||||
console.error(chalk.yellow(`⚠ ${provider} failed: ${error.message}`));
|
||||
|
||||
// Try fallbacks if available
|
||||
if (options.allowFallback !== false && this.config.models?.fallbacks) {
|
||||
return await this.tryFallbacks(prompt, options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Anthropic API directly
|
||||
*/
|
||||
async callAnthropic(prompt, options = {}) {
|
||||
const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('ANTHROPIC_API_KEY not configured');
|
||||
}
|
||||
|
||||
const model = options.model || this.config.model || 'claude-sonnet-4-20250514';
|
||||
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
max_tokens: options.maxTokens || 2000,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Anthropic API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.content[0].text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call OpenRouter API
|
||||
*/
|
||||
async callOpenRouter(prompt, options = {}) {
|
||||
const apiKey = this.config.openrouterApiKey || process.env.OPENROUTER_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENROUTER_API_KEY not configured');
|
||||
}
|
||||
|
||||
const model = options.model || this.config.model || 'google/gemini-flash-1.5';
|
||||
|
||||
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'HTTP-Referer': 'https://github.com/agent-knowledge',
|
||||
'X-Title': 'Agent Knowledge System',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: options.maxTokens || 2000,
|
||||
temperature: options.temperature || 0.3,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`OpenRouter API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.choices || !data.choices[0]) {
|
||||
throw new Error('Invalid response from OpenRouter');
|
||||
}
|
||||
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try fallback providers/models
|
||||
*/
|
||||
async tryFallbacks(prompt, options) {
|
||||
const fallbacks = this.config.models?.fallbacks || [];
|
||||
|
||||
for (const fallback of fallbacks) {
|
||||
try {
|
||||
console.log(chalk.gray(` Trying fallback: ${fallback.provider}/${fallback.model}`));
|
||||
|
||||
return await this.complete(prompt, {
|
||||
...options,
|
||||
provider: fallback.provider,
|
||||
model: fallback.model,
|
||||
allowFallback: false, // Don't recurse
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(chalk.gray(` Fallback failed: ${error.message}`));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('All providers and fallbacks failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models for a provider
|
||||
*/
|
||||
static getAvailableModels(provider) {
|
||||
if (provider === 'openrouter') {
|
||||
return [
|
||||
{ id: 'google/gemini-flash-1.5', name: 'Gemini Flash 1.5', cost: '$0.075/1M' },
|
||||
{ id: 'deepseek/deepseek-chat', name: 'DeepSeek Chat', cost: '$0.14/1M' },
|
||||
{ id: 'anthropic/claude-3-haiku', name: 'Claude 3 Haiku', cost: '$0.25/1M' },
|
||||
{ id: 'mistralai/mistral-small', name: 'Mistral Small', cost: '$0.20/1M' },
|
||||
{ id: 'anthropic/claude-sonnet-4-20250514', name: 'Claude Sonnet', cost: '$3/1M' },
|
||||
{ id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini', cost: '$0.15/1M' },
|
||||
];
|
||||
} else if (provider === 'anthropic') {
|
||||
return [
|
||||
{ id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku', cost: '$0.25/1M' },
|
||||
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet', cost: '$3/1M' },
|
||||
{ id: 'claude-opus-4-20250514', name: 'Claude Opus', cost: '$15/1M' },
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
283
tools/agent-knowledge/templates/update-agents.js
Normal file
283
tools/agent-knowledge/templates/update-agents.js
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Agent Knowledge Update Script
|
||||
*
|
||||
* This script is designed to be run via cron job or CI to update agent memories.
|
||||
* Can also be run manually: node scripts/agents/update-agents.js
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/agents/update-agents.js # Update all agents
|
||||
* node scripts/agents/update-agents.js --dry-run # Preview changes
|
||||
* node scripts/agents/update-agents.js --agent packages/auth # Specific agent
|
||||
*
|
||||
* Cron example (3 AM daily):
|
||||
* 0 3 * * * cd /path/to/project && node scripts/agents/update-agents.js >> .knowledge/cron.log 2>&1
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
||||
|
||||
// Configuration
|
||||
const KNOWLEDGE_DIR = path.join(PROJECT_ROOT, '.knowledge');
|
||||
const CHANGES_FILE = path.join(KNOWLEDGE_DIR, 'changes.jsonl');
|
||||
const REGISTRY_FILE = path.join(KNOWLEDGE_DIR, 'agent-registry.yaml');
|
||||
const CONFIG_FILE = path.join(KNOWLEDGE_DIR, 'config.yaml');
|
||||
const ENV_FILE = path.join(KNOWLEDGE_DIR, '.env');
|
||||
|
||||
async function main() {
|
||||
console.log('🌙 Agent Knowledge Update Script');
|
||||
console.log(` Project: ${PROJECT_ROOT}`);
|
||||
console.log(` Time: ${new Date().toISOString()}\n`);
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const agentFilter = args.find((a) => a.startsWith('--agent='))?.split('=')[1];
|
||||
|
||||
// Load config
|
||||
const config = await loadConfig();
|
||||
console.log(` Provider: ${config.provider}`);
|
||||
console.log(` Model: ${config.model}\n`);
|
||||
|
||||
// Load registry
|
||||
const registry = await loadYaml(REGISTRY_FILE);
|
||||
let agents = registry.agents || [];
|
||||
|
||||
if (agentFilter) {
|
||||
agents = agents.filter((a) => a.path.includes(agentFilter));
|
||||
console.log(` Filtering to agent: ${agentFilter}`);
|
||||
}
|
||||
|
||||
if (agents.length === 0) {
|
||||
console.log('No agents to update.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Read changes
|
||||
const changes = await readChanges();
|
||||
if (changes.length === 0) {
|
||||
console.log('No pending changes to process.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📊 Processing ${changes.length} changes for ${agents.length} agent(s)...\n`);
|
||||
|
||||
// Group changes by agent
|
||||
const changesByAgent = groupChangesByAgent(changes, agents);
|
||||
|
||||
// Update each agent
|
||||
let updated = 0;
|
||||
for (const agent of agents) {
|
||||
const agentChanges = changesByAgent[agent.agent_dir] || [];
|
||||
if (agentChanges.length === 0) {
|
||||
console.log(`⏭️ ${agent.name}: No relevant changes`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🤖 ${agent.name}: ${agentChanges.length} changes`);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(' [DRY RUN] Would update memory');
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateAgentMemory(agent, agentChanges, config);
|
||||
console.log(` ✓ Updated memory.md`);
|
||||
updated++;
|
||||
} catch (error) {
|
||||
console.error(` ✗ Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Archive changes
|
||||
if (!dryRun && updated > 0) {
|
||||
await archiveChanges();
|
||||
console.log('\n📦 Changes archived');
|
||||
}
|
||||
|
||||
console.log(`\n✅ Updated ${updated} agent(s)`);
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
const config = {
|
||||
provider: process.env.LLM_PROVIDER || 'openrouter',
|
||||
model: process.env.OPENROUTER_MODEL || process.env.ANTHROPIC_MODEL || 'google/gemini-flash-1.5',
|
||||
openrouterApiKey: process.env.OPENROUTER_API_KEY,
|
||||
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
||||
};
|
||||
|
||||
// Load from .env file
|
||||
try {
|
||||
const envContent = await fs.readFile(ENV_FILE, 'utf-8');
|
||||
for (const line of envContent.split('\n')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length > 0) {
|
||||
const value = valueParts.join('=').trim();
|
||||
if (key.trim() === 'OPENROUTER_API_KEY') config.openrouterApiKey = value;
|
||||
if (key.trim() === 'ANTHROPIC_API_KEY') config.anthropicApiKey = value;
|
||||
if (key.trim() === 'LLM_PROVIDER') config.provider = value;
|
||||
if (key.trim() === 'OPENROUTER_MODEL') config.model = value;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// .env file not found, use defaults
|
||||
}
|
||||
|
||||
// Load from config.yaml
|
||||
try {
|
||||
const yamlConfig = await loadYaml(CONFIG_FILE);
|
||||
if (yamlConfig.provider) config.provider = yamlConfig.provider;
|
||||
if (yamlConfig.model) config.model = yamlConfig.model;
|
||||
} catch (e) {
|
||||
// config.yaml not found
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function loadYaml(filePath) {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
// Simple YAML parser for our use case
|
||||
const result = { agents: [] };
|
||||
let currentAgent = null;
|
||||
|
||||
for (const line of content.split('\n')) {
|
||||
if (line.trim().startsWith('- path:')) {
|
||||
if (currentAgent) result.agents.push(currentAgent);
|
||||
currentAgent = { path: line.split(':')[1].trim(), watches: [] };
|
||||
} else if (currentAgent) {
|
||||
if (line.includes('agent_dir:')) currentAgent.agent_dir = line.split(':')[1].trim();
|
||||
if (line.includes('name:')) currentAgent.name = line.split(':').slice(1).join(':').trim();
|
||||
if (line.trim().startsWith('- ') && !line.includes(':')) {
|
||||
currentAgent.watches.push(line.trim().slice(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentAgent) result.agents.push(currentAgent);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function readChanges() {
|
||||
try {
|
||||
const content = await fs.readFile(CHANGES_FILE, 'utf-8');
|
||||
return content
|
||||
.split('\n')
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function groupChangesByAgent(changes, agents) {
|
||||
const grouped = {};
|
||||
for (const agent of agents) {
|
||||
grouped[agent.agent_dir] = changes.filter((change) =>
|
||||
agent.watches.some((pattern) => {
|
||||
const regex = new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*'));
|
||||
return regex.test(change.file);
|
||||
})
|
||||
);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
async function updateAgentMemory(agent, changes, config) {
|
||||
const memoryPath = path.join(PROJECT_ROOT, agent.agent_dir, 'memory.md');
|
||||
let existingMemory = '';
|
||||
try {
|
||||
existingMemory = await fs.readFile(memoryPath, 'utf-8');
|
||||
} catch {
|
||||
/* new file */
|
||||
}
|
||||
|
||||
const byDate = {};
|
||||
for (const c of changes) {
|
||||
const date = c.timestamp?.split('T')[0] || 'unknown';
|
||||
byDate[date] = byDate[date] || [];
|
||||
byDate[date].push({ file: c.file, message: c.message, author: c.author });
|
||||
}
|
||||
|
||||
const prompt = `You are updating the memory file for "${agent.name}".
|
||||
|
||||
## Existing Memory
|
||||
${existingMemory || '(empty)'}
|
||||
|
||||
## Recent Changes
|
||||
${JSON.stringify(byDate, null, 2)}
|
||||
|
||||
## Instructions
|
||||
Update the memory file:
|
||||
- Add dated section at TOP for new learnings
|
||||
- Be concise, capture important context
|
||||
- Keep under 400 lines
|
||||
|
||||
Output ONLY the updated memory.md content.`;
|
||||
|
||||
const updatedMemory = await callLLM(prompt, config);
|
||||
await fs.writeFile(memoryPath, updatedMemory);
|
||||
}
|
||||
|
||||
async function callLLM(prompt, config) {
|
||||
if (config.provider === 'openrouter') {
|
||||
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.openrouterApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 3000,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
} else {
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': config.anthropicApiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model,
|
||||
max_tokens: 3000,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.content[0].text;
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveChanges() {
|
||||
const archiveDir = path.join(KNOWLEDGE_DIR, 'archive');
|
||||
await fs.mkdir(archiveDir, { recursive: true });
|
||||
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
const archivePath = path.join(archiveDir, `changes-${date}.jsonl`);
|
||||
|
||||
const content = await fs.readFile(CHANGES_FILE, 'utf-8');
|
||||
await fs.appendFile(archivePath, content);
|
||||
await fs.writeFile(CHANGES_FILE, '');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Fatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue