Initial commit: Claude Session Manager with complete CLI toolset
- Add TypeScript project structure with Bun runtime - Implement CLI commands for session, project, stats, and optimization - Add SQLite database integration with prepared statements - Include AGENTS.md for development guidelines - Add Makefile for common development tasks - Configure ESLint and TypeScript with strict mode
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
],
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
'no-unused-vars': 'off', // Turn off base rule as it conflicts with @typescript-eslint version
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true,
|
||||
},
|
||||
globals: {
|
||||
Bun: 'readonly',
|
||||
},
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
data/*.db
|
||||
data/*.db-journal
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Guidelines
|
||||
|
||||
## Build Commands
|
||||
- `bun run build` - Build the project (runs typecheck first)
|
||||
- `bun run dev` - Development mode with hot reload
|
||||
- `bun run test` - Run all tests
|
||||
- `bun run lint` - Run ESLint on TypeScript files
|
||||
- `bun run typecheck` - Run TypeScript type checking without emitting
|
||||
|
||||
## Code Style Guidelines
|
||||
- Use TypeScript with strict mode enabled (noImplicitAny: false, strictNullChecks: false)
|
||||
- Import style: ES6 imports with `import { X } from './path'` for named exports
|
||||
- Use `@/*` path alias for src directory imports
|
||||
- Naming: camelCase for variables/functions, PascalCase for classes/interfaces
|
||||
- File structure: Organize by domain (cli/, database/, models/, services/, templates/)
|
||||
- Error handling: Use try/catch blocks, log errors with logger utility
|
||||
- Database: Use SQLite with prepared statements, map rows to interfaces
|
||||
- CLI: Use Commander.js for command structure, chalk for colored output
|
||||
- Date handling: Use moment.js for date formatting/manipulation
|
||||
- UUID generation: Use uuid v4 for IDs
|
||||
- No explicit return types required (inferred)
|
||||
- Prefer const over let, avoid var
|
||||
- Use Bun runtime features where appropriate
|
||||
@@ -0,0 +1,50 @@
|
||||
.PHONY: help build dev test lint typecheck clean start install
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " install Install dependencies"
|
||||
@echo " build Build the project"
|
||||
@echo " dev Run in development mode with hot reload"
|
||||
@echo " start Run the built application"
|
||||
@echo " test Run all tests"
|
||||
@echo " lint Run ESLint"
|
||||
@echo " typecheck Run TypeScript type checking"
|
||||
@echo " clean Clean build artifacts"
|
||||
@echo " help Show this help message"
|
||||
|
||||
# Install dependencies
|
||||
install:
|
||||
bun install
|
||||
|
||||
# Build the project (includes typecheck)
|
||||
build:
|
||||
bun run build
|
||||
|
||||
# Development mode with hot reload
|
||||
dev:
|
||||
bun run dev
|
||||
|
||||
# Run the built application
|
||||
start:
|
||||
bun run start
|
||||
|
||||
# Run all tests
|
||||
test:
|
||||
bun run test
|
||||
|
||||
# Run ESLint
|
||||
lint:
|
||||
bun run lint
|
||||
|
||||
# Run TypeScript type checking
|
||||
typecheck:
|
||||
bun run typecheck
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
bun run clean
|
||||
|
||||
# Development workflow: install, typecheck, lint, test
|
||||
check: install typecheck lint test
|
||||
@echo "All checks passed!"
|
||||
@@ -0,0 +1,36 @@
|
||||
Here are the best tips and best practices for maximizing your Claude Code usage and avoiding those dreaded limits:
|
||||
|
||||
### 1. Master Your Context (The Token Saver)
|
||||
|
||||
Every message you send re-sends the entire conversation history to Claude, which is what eats up your tokens. The goal is to keep this history short and clean.
|
||||
|
||||
* **Use `/clear` Frequently:** This is the most crucial command. Use it to completely wipe the current conversation history when you finish a task, switch to an unrelated bug, or start a new feature.
|
||||
* **Tip:** When you clear, have Claude first **write a summary** of the finished work to a file (e.g., `session-notes.md`). Then, in the new session, simply reference that file to catch Claude up without re-sending the hundreds of messages.
|
||||
* **Use `/compact`:** If the conversation is getting long but you still need the history, use the `/compact` command. Claude will summarize the history, reducing the token count while preserving key context.
|
||||
* **Leverage `CLAUDE.md` for Long-Term Memory:** Create a `CLAUDE.md` file in your project root. Claude Code reads this automatically at the start of every session. Use it for:
|
||||
* Project architecture and tech stack.
|
||||
* Coding style guides (e.g., "Use snake\_case for APIs").
|
||||
* Common commands (e.g., `npm run test`).
|
||||
* **This saves you tokens** by preventing you from repeating the same instructions or context in every new chat.
|
||||
* **Target Your Files:** Instead of letting Claude try to search for the right file, explicitly mention the files it needs to read or modify using the `@` symbol or full file paths. This prevents Claude from wasting tokens reading unnecessary parts of your codebase.
|
||||
|
||||
### 2. Optimize Your Workflow (The Efficiency Booster)
|
||||
|
||||
* **Batch Your Requests:** Group similar requests or steps into a single, detailed prompt.
|
||||
* *Instead of:* 1. "Fix the typo in the config file." 2. "Now, run the tests." 3. "Commit the change."
|
||||
* *Use:* "Fix the typo in the config file, run the unit tests, and if they pass, commit the change with the message 'fix: corrected config typo'."
|
||||
* **Use the Right Model for the Job:**
|
||||
* **Claude Opus (Max Plans):** Reserve Opus for high-level, complex tasks like architecture planning, intricate debugging, or large refactors. Opus consumes your allowance much faster.
|
||||
* **Claude Sonnet:** Use Sonnet for implementation, writing boilerplate code, documentation, or routine fixes. It offers a much higher number of prompts per session.
|
||||
* **Adopt a "Plan First" Approach:**
|
||||
1. **Plan:** Ask Claude to first create a detailed, step-by-step plan for the task. You can use phrases like `"think hard"` to give it a higher computation budget for this step.
|
||||
2. **Review:** Review and approve the plan.
|
||||
3. **Execute:** Give the final go-ahead. This avoids wasting tokens on a long, iterative conversation if Claude was on the wrong path from the start.
|
||||
|
||||
### 3. Be Strategic with the 5-Hour Window (The Time Hack)
|
||||
|
||||
* **The Overlap Hack:** If you know you have an intensive work session coming up, you can deliberately send a very short message 2-3 hours before you start your main work. This starts the 5-hour timer for your first session. When your first session allowance is depleted, the second 5-hour session will be due for a reset, allowing you to seamlessly start fresh and continue working without a prolonged break.
|
||||
* **Cycle Your Work:** When your Claude Code session runs out, switch to tasks that do **not** require the AI (e.g., meeting prep, email, manual testing, or architecture review). Come back to Claude when the 5-hour window has reset.
|
||||
|
||||
These strategies allow you to treat Claude Code like a powerful, highly-focused partner, giving it concentrated work blocks to maximize your session usage.
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
# Claude Session Manager
|
||||
|
||||
A powerful tool to manage and optimize your Claude Code programming sessions, implementing all the best practices from [PLAN.md](./PLAN.md).
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 Session Management
|
||||
- **5-Hour Window Tracking**: Automatically manages Claude's 5-hour session windows
|
||||
- **Overlap Hack**: Schedule overlap sessions for extended work periods
|
||||
- **Smart Scheduling**: Optimize session timing based on usage patterns
|
||||
- **Active Session Monitoring**: Real-time tracking of current sessions
|
||||
|
||||
### 📊 Token Optimization
|
||||
- **Usage Analytics**: Detailed token consumption tracking
|
||||
- **Optimization Recommendations**: AI-powered suggestions to reduce token usage
|
||||
- **Efficiency Scoring**: Quantitative measurement of session efficiency
|
||||
- **Cost Analysis**: Track token usage by model and project
|
||||
|
||||
### 🗂️ Project Context Management
|
||||
- **CLAUDE.md Generation**: Auto-generate project context files
|
||||
- **Tech Stack Detection**: Automatically identify project technologies
|
||||
- **Coding Style Analysis**: Detect and document coding conventions
|
||||
- **Common Commands**: Track frequently used project commands
|
||||
|
||||
### 📝 Smart Note-Taking
|
||||
- **Session Summaries**: Automatic generation of session summaries
|
||||
- **Quick Notes**: Categorized note-taking during sessions
|
||||
- **Searchable History**: Find information across all sessions
|
||||
- **Export Capabilities**: Export session history in various formats
|
||||
|
||||
### 🚀 Workflow Optimization
|
||||
- **Task Scheduling**: Optimal scheduling for upcoming tasks
|
||||
- **Model Recommendations**: AI-powered model selection suggestions
|
||||
- **Batching Suggestions**: Recommendations for grouping similar tasks
|
||||
- **Anomaly Detection**: Identify workflow inefficiencies
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd claude-session-manager
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build the project
|
||||
npm run build
|
||||
|
||||
# Install globally (optional)
|
||||
npm install -g .
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create a Project
|
||||
```bash
|
||||
csm project create --name "My Project" --path "/path/to/project"
|
||||
```
|
||||
|
||||
### 2. Start a Session
|
||||
```bash
|
||||
csm session start --project <project-id> --model sonnet
|
||||
```
|
||||
|
||||
### 3. Track Usage
|
||||
```bash
|
||||
csm session track <session-id> <tokens> <messages>
|
||||
```
|
||||
|
||||
### 4. End Session
|
||||
```bash
|
||||
csm session end --summary "Implemented user authentication"
|
||||
```
|
||||
|
||||
### 5. View Statistics
|
||||
```bash
|
||||
csm stats today
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
|
||||
### Project Management
|
||||
```bash
|
||||
# Create new project
|
||||
csm project create [-n <name>] [-p <path>]
|
||||
|
||||
# List all projects
|
||||
csm project list
|
||||
|
||||
# Show project details
|
||||
csm project info <project-id>
|
||||
|
||||
# Generate CLAUDE.md
|
||||
csm project claude-md <project-id>
|
||||
|
||||
# View session summaries
|
||||
csm project notes <project-id> [-l <limit>]
|
||||
|
||||
# Export session history
|
||||
csm project export <project-id> [-f <format>] [-o <output>]
|
||||
```
|
||||
|
||||
### Session Management
|
||||
```bash
|
||||
# Start new session
|
||||
csm session start [-p <project-id>] [-m <model>]
|
||||
|
||||
# End session
|
||||
csm session end [-i <session-id>] [-s <summary>]
|
||||
|
||||
# Track token usage
|
||||
csm session track <session-id> <tokens> <messages>
|
||||
|
||||
# Add note to session
|
||||
csm session note <note> [-t <type>] [-i <session-id>]
|
||||
|
||||
# List recent sessions
|
||||
csm session list [-l <limit>] [-p <project-id>]
|
||||
|
||||
# Check session status
|
||||
csm session status
|
||||
|
||||
# Schedule overlap hack
|
||||
csm session overlap [-d <hours>]
|
||||
```
|
||||
|
||||
### Statistics & Analytics
|
||||
```bash
|
||||
# Show usage statistics
|
||||
csm stats usage [-d <days>]
|
||||
|
||||
# Today's usage and recommendations
|
||||
csm stats today
|
||||
|
||||
# Workflow efficiency analysis
|
||||
csm stats optimize [-p <project-id>]
|
||||
|
||||
# Detect anomalies and trends
|
||||
csm stats anomalies
|
||||
|
||||
# Token usage breakdown
|
||||
csm stats tokens [-p <project-id>]
|
||||
```
|
||||
|
||||
### Optimization Tools
|
||||
```bash
|
||||
# Get suggestions for specific task
|
||||
csm optimize suggest <task> [-c <complexity>]
|
||||
|
||||
# Generate optimal schedule
|
||||
csm optimize schedule
|
||||
|
||||
# Analyze overall workflow
|
||||
csm optimize workflow [-p <project-id>]
|
||||
|
||||
# Show optimization tips
|
||||
csm optimize tips
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Daily Workflow
|
||||
```bash
|
||||
# Start your day by checking status
|
||||
csm session status
|
||||
|
||||
# Start a new session for feature development
|
||||
csm session start --project my-web-app --model sonnet
|
||||
|
||||
# During development, add quick notes
|
||||
csm session note "Fixed login bug" --type bug
|
||||
csm session note "Add password validation" --type feature
|
||||
|
||||
# Track usage periodically
|
||||
csm session track abc123 25000 15
|
||||
|
||||
# End session with summary
|
||||
csm session end --summary "Implemented user authentication with password validation"
|
||||
|
||||
# Check today's optimization recommendations
|
||||
csm stats today
|
||||
```
|
||||
|
||||
### Example 2: Complex Architecture Task
|
||||
```bash
|
||||
# Get optimization suggestions first
|
||||
csm optimize suggest "Design microservices architecture" --complexity complex
|
||||
|
||||
# Start with Opus for complex task
|
||||
csm session start --project my-system --model opus
|
||||
|
||||
# Schedule overlap hack for extended work
|
||||
csm session overlap --delay 2
|
||||
|
||||
# End with detailed summary
|
||||
csm session end --summary "Designed comprehensive microservices architecture with service mesh"
|
||||
```
|
||||
|
||||
### Example 3: Project Setup
|
||||
```bash
|
||||
# Create new project
|
||||
csm project create --name "E-commerce Platform" --path ./ecommerce
|
||||
|
||||
# Generate CLAUDE.md for context
|
||||
csm project claude-md "E-commerce Platform"
|
||||
|
||||
# View project info
|
||||
csm project info "E-commerce Platform"
|
||||
```
|
||||
|
||||
## Best Practices (from PLAN.md)
|
||||
|
||||
### Context Management
|
||||
- **Use `/clear` frequently** - Most crucial command for token savings
|
||||
- **Use `/compact`** for long but necessary conversations
|
||||
- **Leverage `CLAUDE.md`** for long-term project memory
|
||||
- **Target specific files** with `@` symbol or full paths
|
||||
|
||||
### Workflow Optimization
|
||||
- **Batch similar requests** into single detailed prompts
|
||||
- **Use Sonnet for implementation**, Opus for complex tasks
|
||||
- **Adopt "Plan First" approach** - plan, review, execute
|
||||
- **Use "think hard"** for complex planning tasks
|
||||
|
||||
### 5-Hour Window Strategy
|
||||
- **Use overlap hack** 2-3 hours before intensive work
|
||||
- **Cycle to non-AI tasks** when session runs out
|
||||
- **Schedule complex tasks** for fresh sessions
|
||||
- **Track window timing** to maximize productivity
|
||||
|
||||
## Configuration
|
||||
|
||||
The tool stores all data in a local SQLite database (`./data/claude-sessions.db`). Configuration options:
|
||||
|
||||
- Database location: Can be overridden with environment variable `CSM_DB_PATH`
|
||||
- Default model: Sonnet (recommended for most tasks)
|
||||
- Token limits: Configurable in service classes
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Workflow Automation
|
||||
```bash
|
||||
# Generate optimal schedule for multiple tasks
|
||||
csm optimize schedule
|
||||
# Enter your tasks interactively
|
||||
# Get optimized schedule with model recommendations
|
||||
|
||||
# Analyze workflow efficiency
|
||||
csm optimize workflow
|
||||
# Get comprehensive analysis with actionable recommendations
|
||||
```
|
||||
|
||||
### Token Optimization
|
||||
```bash
|
||||
# Get personalized recommendations
|
||||
csm stats today
|
||||
# See high-priority optimization suggestions
|
||||
|
||||
# Track token efficiency trends
|
||||
csm stats usage --days 30
|
||||
# Monitor your optimization progress over time
|
||||
```
|
||||
|
||||
### Project Intelligence
|
||||
```bash
|
||||
# Auto-generate comprehensive project context
|
||||
csm project claude-md <project-id>
|
||||
# Includes tech stack, coding style, architecture, and common commands
|
||||
|
||||
# Export session history for documentation
|
||||
csm project export <project-id> --format markdown --output docs/session-history.md
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Database errors**: Ensure write permissions to `./data/` directory
|
||||
2. **Project not found**: Use `csm project list` to see available projects
|
||||
3. **Session tracking**: Always track tokens before ending session for accurate statistics
|
||||
|
||||
### Getting Help
|
||||
|
||||
```bash
|
||||
# Show all commands
|
||||
csm --help
|
||||
|
||||
# Show command-specific help
|
||||
csm session --help
|
||||
csm project --help
|
||||
csm stats --help
|
||||
csm optimize --help
|
||||
|
||||
# Show optimization tips
|
||||
csm optimize tips
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
This tool implements the best practices from PLAN.md. Contributions welcome for:
|
||||
|
||||
- Additional optimization algorithms
|
||||
- New workflow automation features
|
||||
- Enhanced analytics and reporting
|
||||
- Integration with other development tools
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
@@ -0,0 +1,404 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "claude-session-manager",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.1.0",
|
||||
"inquirer": "^9.2.12",
|
||||
"moment": "^2.29.4",
|
||||
"table": "^6.8.1",
|
||||
"uuid": "^9.0.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/inquirer": "^9.0.7",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||
"@typescript-eslint/parser": "^6.13.0",
|
||||
"bun-types": "^1.2.20",
|
||||
"eslint": "^8.54.0",
|
||||
"typescript": "^5.3.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
|
||||
|
||||
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
|
||||
|
||||
"@inquirer/external-editor": ["@inquirer/external-editor@1.0.2", "", { "dependencies": { "chardet": "^2.1.0", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ=="],
|
||||
|
||||
"@inquirer/figures": ["@inquirer/figures@1.0.14", "", {}, "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@types/inquirer": ["@types/inquirer@9.0.9", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||
|
||||
"@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="],
|
||||
|
||||
"@types/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="],
|
||||
|
||||
"@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@6.21.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/type-utils": "6.21.0", "@typescript-eslint/utils": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@6.21.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@6.21.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@6.21.0", "", {}, "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@6.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
|
||||
|
||||
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
|
||||
|
||||
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
||||
|
||||
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
|
||||
|
||||
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
|
||||
|
||||
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||
|
||||
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
|
||||
|
||||
"globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"inquirer": ["inquirer@9.3.8", "", { "dependencies": { "@inquirer/external-editor": "^1.0.2", "@inquirer/figures": "^1.0.3", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "1.0.0", "ora": "^5.4.1", "run-async": "^3.0.0", "rxjs": "^7.8.1", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="],
|
||||
|
||||
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
|
||||
|
||||
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||
|
||||
"run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
|
||||
"slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="],
|
||||
|
||||
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="],
|
||||
|
||||
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"table/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
[install]
|
||||
# Use npm registry
|
||||
registry = "https://registry.npmjs.org/"
|
||||
# Cache in node_modules for compatibility
|
||||
cache = true
|
||||
|
||||
[run]
|
||||
# Enable hot reload by default
|
||||
hot = true
|
||||
|
||||
[test]
|
||||
# Enable test coverage
|
||||
coverage = true
|
||||
|
||||
[build]
|
||||
# Target current Node.js version for compatibility
|
||||
target = "node"
|
||||
# Minify production builds
|
||||
minify = true
|
||||
# Enable sourcemaps for debugging
|
||||
sourcemap = true
|
||||
|
||||
# Performance optimizations
|
||||
[optimize]
|
||||
# Enable Bun's optimizer
|
||||
enabled = true
|
||||
|
||||
# Lockfile configuration
|
||||
[lockfile]
|
||||
# Save exact versions
|
||||
save = true
|
||||
# Print install summary
|
||||
print = "yarn"
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "claude-session-manager",
|
||||
"version": "1.0.0",
|
||||
"description": "A tool to manage Claude Code programming sessions with optimization and scheduling",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"csm": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun build ./src/index.ts --outdir ./dist --target node --minify --sourcemap",
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"start": "bun dist/index.js",
|
||||
"test": "bun test",
|
||||
"lint": "bunx eslint src/**/*.ts",
|
||||
"typecheck": "bun tsc --noEmit",
|
||||
"clean": "rm -rf dist",
|
||||
"prebuild": "bun run typecheck",
|
||||
"postbuild": "chmod +x dist/index.js"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
"session",
|
||||
"management",
|
||||
"optimization",
|
||||
"ai"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.1.0",
|
||||
"inquirer": "^9.2.12",
|
||||
"moment": "^2.29.4",
|
||||
"table": "^6.8.1",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/inquirer": "^9.0.7",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||
"@typescript-eslint/parser": "^6.13.0",
|
||||
"bun-types": "^1.2.20",
|
||||
"eslint": "^8.54.0",
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { Database } from '../../database/Database';
|
||||
import { WorkflowOptimizer } from '../../services/WorkflowOptimizer';
|
||||
import { SessionManager } from '../../services/SessionManager';
|
||||
import { TokenTracker } from '../../services/TokenTracker';
|
||||
import inquirer from 'inquirer';
|
||||
import { table } from 'table';
|
||||
import moment from 'moment';
|
||||
|
||||
export function optimizeCommands(program: Command): void {
|
||||
const db = new Database();
|
||||
const sessionManager = new SessionManager(db);
|
||||
const tokenTracker = new TokenTracker(db);
|
||||
const workflowOptimizer = new WorkflowOptimizer(db, tokenTracker, sessionManager);
|
||||
|
||||
const optimizeCmd = program.command('optimize').description('Optimize your Claude workflow');
|
||||
|
||||
optimizeCmd
|
||||
.command('suggest')
|
||||
.description('Get optimization suggestions for a task')
|
||||
.argument('<task>', 'Task description')
|
||||
.option('-c, --complexity <complexity>', 'Task complexity (simple|medium|complex)', 'medium')
|
||||
.action(async (task, options) => {
|
||||
try {
|
||||
const suggestion = await workflowOptimizer.suggestOptimalSession(task, options.complexity);
|
||||
|
||||
console.log(chalk.cyan('\\nOptimization Suggestions:'));
|
||||
console.log(chalk.gray('─'.repeat(40)));
|
||||
|
||||
console.log(chalk.yellow(`\\nTask: ${task}`));
|
||||
console.log(chalk.cyan(`\\nRecommended Model: ${suggestion.modelType.toUpperCase()}`));
|
||||
console.log(`Estimated Tokens: ${suggestion.estimatedTokens.toLocaleString()}`);
|
||||
|
||||
if (suggestion.batchingSuggestions.length > 0) {
|
||||
console.log(chalk.cyan('\\nBatching Suggestions:'));
|
||||
suggestion.batchingSuggestions.forEach((suggestion, index) => {
|
||||
console.log(`${index + 1}. ${suggestion}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (suggestion.preparationTips.length > 0) {
|
||||
console.log(chalk.cyan('\\nPreparation Tips:'));
|
||||
suggestion.preparationTips.forEach((tip, index) => {
|
||||
console.log(`${index + 1}. ${tip}`);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error getting suggestions:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
optimizeCmd
|
||||
.command('schedule')
|
||||
.description('Generate optimal schedule for upcoming tasks')
|
||||
.action(async () => {
|
||||
try {
|
||||
console.log(chalk.cyan('Enter your upcoming tasks (one per line). Empty line to finish:'));
|
||||
|
||||
const tasks = [];
|
||||
let taskCount = 0;
|
||||
|
||||
while (true) {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: `Task ${taskCount + 1}:`,
|
||||
when: () => taskCount === 0 || tasks.length > 0
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'complexity',
|
||||
message: 'Complexity:',
|
||||
choices: ['simple', 'medium', 'complex'],
|
||||
when: (answers) => answers.description !== ''
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'duration',
|
||||
message: 'Estimated duration (minutes):',
|
||||
validate: (input) => !isNaN(input) && parseInt(input) > 0 || 'Please enter a valid number',
|
||||
when: (answers) => answers.description !== ''
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'priority',
|
||||
message: 'Priority:',
|
||||
choices: ['high', 'medium', 'low'],
|
||||
when: (answers) => answers.description !== ''
|
||||
}
|
||||
]);
|
||||
|
||||
if (answers.description === '') {
|
||||
break;
|
||||
}
|
||||
|
||||
tasks.push({
|
||||
description: answers.description,
|
||||
complexity: answers.complexity,
|
||||
estimatedDuration: parseInt(answers.duration),
|
||||
priority: answers.priority
|
||||
});
|
||||
|
||||
taskCount++;
|
||||
|
||||
const { continue: shouldContinue } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'continue',
|
||||
message: 'Add another task?',
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
|
||||
if (!shouldContinue) break;
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
console.log(chalk.yellow('No tasks entered.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const schedule = await workflowOptimizer.generateOptimalSchedule(tasks);
|
||||
|
||||
console.log(chalk.cyan('\\nOptimal Schedule:'));
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
|
||||
const tableData = [
|
||||
['Time', 'Task', 'Model', 'Grouping']
|
||||
];
|
||||
|
||||
schedule.schedule.forEach(item => {
|
||||
tableData.push([
|
||||
moment(item.recommendedTime).format('HH:mm'),
|
||||
item.task.substring(0, 30) + (item.task.length > 30 ? '...' : ''),
|
||||
item.modelType.toUpperCase(),
|
||||
item.grouping
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table(tableData));
|
||||
|
||||
console.log(chalk.cyan(`\\nTotal Estimated Tokens: ${schedule.totalEstimatedTokens.toLocaleString()}`));
|
||||
|
||||
if (schedule.optimizationNotes.length > 0) {
|
||||
console.log(chalk.yellow('\\nOptimization Notes:'));
|
||||
schedule.optimizationNotes.forEach(note => {
|
||||
console.log(`• ${note}`);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error generating schedule:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
optimizeCmd
|
||||
.command('workflow')
|
||||
.description('Analyze and optimize your overall workflow')
|
||||
.option('-p, --project <projectId>', 'Focus on specific project')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const analysis = await workflowOptimizer.analyzeWorkflowEfficiency(options.project);
|
||||
|
||||
console.log(chalk.cyan('\\n🔍 Workflow Analysis'));
|
||||
console.log(chalk.gray('─'.repeat(40)));
|
||||
|
||||
// Score with visual indicator
|
||||
const score = analysis.score;
|
||||
let emoji, color;
|
||||
if (score >= 80) {
|
||||
emoji = '🟢';
|
||||
color = chalk.green;
|
||||
} else if (score >= 60) {
|
||||
emoji = '🟡';
|
||||
color = chalk.yellow;
|
||||
} else {
|
||||
emoji = '🔴';
|
||||
color = chalk.red;
|
||||
}
|
||||
|
||||
console.log(`\\n${emoji} Efficiency Score: ${color(`${score}/100`)}`);
|
||||
|
||||
// Priority recommendations
|
||||
if (analysis.recommendations.length > 0) {
|
||||
console.log(chalk.cyan('\\n🎯 Priority Actions:'));
|
||||
|
||||
const highPriority = analysis.recommendations.filter(r => r.priority === 'high');
|
||||
const mediumPriority = analysis.recommendations.filter(r => r.priority === 'medium');
|
||||
|
||||
if (highPriority.length > 0) {
|
||||
console.log(chalk.red('\\nHigh Priority:'));
|
||||
highPriority.forEach((rec, index) => {
|
||||
console.log(`${index + 1}. ${rec.message}`);
|
||||
if (rec.potentialSavings) {
|
||||
console.log(chalk.gray(` 💰 Save ${rec.potentialSavings.toLocaleString()} tokens`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (mediumPriority.length > 0) {
|
||||
console.log(chalk.yellow('\\nMedium Priority:'));
|
||||
mediumPriority.forEach((rec, index) => {
|
||||
console.log(`${index + 1}. ${rec.message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Quick wins
|
||||
console.log(chalk.cyan('\\n⚡ Quick Wins:'));
|
||||
console.log('• Use /clear before starting unrelated tasks');
|
||||
console.log('• Batch similar requests into single prompts');
|
||||
console.log('• Use Sonnet for implementation, Opus for architecture');
|
||||
console.log('• Schedule overlap hack for intensive work sessions');
|
||||
|
||||
// Insights
|
||||
if (analysis.insights.length > 0) {
|
||||
console.log(chalk.cyan('\\n💡 Key Insights:'));
|
||||
analysis.insights.forEach(insight => {
|
||||
console.log(`• ${insight}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Next steps
|
||||
console.log(chalk.cyan('\\n📋 Next Steps:'));
|
||||
console.log('1. Implement high-priority recommendations first');
|
||||
console.log('2. Set up overlap hack for your next intensive session');
|
||||
console.log('3. Review and update your project CLAUDE.md files');
|
||||
console.log('4. Use "csm optimize suggest" before starting new tasks');
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error analyzing workflow:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
optimizeCmd
|
||||
.command('tips')
|
||||
.description('Show optimization tips based on PLAN.md guidelines')
|
||||
.action(async () => {
|
||||
try {
|
||||
console.log(chalk.cyan('\\n🚀 Claude Code Optimization Tips'));
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
|
||||
console.log(chalk.yellow('\\n📚 Context Management (Token Savers):'));
|
||||
console.log('• Use /clear frequently - most crucial command');
|
||||
console.log('• Use /compact for long but needed conversations');
|
||||
console.log('• Leverage CLAUDE.md for long-term project memory');
|
||||
console.log('• Target specific files with @ symbol or paths');
|
||||
|
||||
console.log(chalk.yellow('\\n⚡ Workflow Optimization:'));
|
||||
console.log('• Batch similar requests into single detailed prompts');
|
||||
console.log('• Use Sonnet for implementation, Opus for complex tasks');
|
||||
console.log('• Adopt "Plan First" approach - plan, review, execute');
|
||||
console.log('• Use "think hard" for complex planning tasks');
|
||||
|
||||
console.log(chalk.yellow('\\n⏰ 5-Hour Window Strategy:'));
|
||||
console.log('• Use overlap hack 2-3 hours before intensive work');
|
||||
console.log('• Cycle to non-AI tasks when session runs out');
|
||||
console.log('• Schedule complex tasks for fresh sessions');
|
||||
console.log('• Track window timing to maximize productivity');
|
||||
|
||||
console.log(chalk.yellow('\\n🎯 Best Practices:'));
|
||||
console.log('• Write session summaries before using /clear');
|
||||
console.log('• Reference summary files instead of long histories');
|
||||
console.log('• Group implementation tasks together');
|
||||
console.log('• Save architecture work for Opus sessions');
|
||||
|
||||
console.log(chalk.green('\\n💡 Pro Tip:'));
|
||||
console.log('Create a routine: Start with optimization check, work in focused blocks, end with summary');
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error showing tips:'), error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { Database } from '../../database/Database';
|
||||
import { ProjectManager } from '../../services/ProjectManager';
|
||||
import { NoteTaker } from '../../services/NoteTaker';
|
||||
import inquirer from 'inquirer';
|
||||
import { table } from 'table';
|
||||
import { existsSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export function projectCommands(program: Command): void {
|
||||
const db = new Database();
|
||||
const projectManager = new ProjectManager(db);
|
||||
const noteTaker = new NoteTaker(db);
|
||||
|
||||
const projectCmd = program.command('project').description('Manage projects');
|
||||
|
||||
projectCmd
|
||||
.command('create')
|
||||
.description('Create a new project')
|
||||
.option('-n, --name <name>', 'Project name')
|
||||
.option('-p, --path <path>', 'Project path')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
let { name, path: projectPath } = options;
|
||||
|
||||
if (!name || !projectPath) {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Project name:',
|
||||
when: !name,
|
||||
validate: (input) => input.trim() !== '' || 'Project name is required'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'path',
|
||||
message: 'Project path:',
|
||||
when: !projectPath,
|
||||
default: process.cwd(),
|
||||
validate: (input) => existsSync(input) || 'Path does not exist'
|
||||
}
|
||||
]);
|
||||
name = answers.name;
|
||||
projectPath = answers.path;
|
||||
}
|
||||
|
||||
const project = await projectManager.createProject(name, projectPath);
|
||||
|
||||
console.log(chalk.green('✓ Project created successfully!'));
|
||||
console.log(chalk.cyan(`Name: ${project.name}`));
|
||||
console.log(chalk.cyan(`Path: ${project.path}`));
|
||||
console.log(chalk.cyan(`Tech Stack: ${project.techStack.join(', ')}`));
|
||||
|
||||
// Ask if user wants to generate CLAUDE.md
|
||||
const { generateClaudeMd } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'generateClaudeMd',
|
||||
message: 'Generate CLAUDE.md file?',
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
|
||||
if (generateClaudeMd) {
|
||||
await projectManager.generateClaudeMd(project.id);
|
||||
console.log(chalk.green('✓ CLAUDE.md generated successfully!'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error creating project:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
projectCmd
|
||||
.command('list')
|
||||
.description('List all projects')
|
||||
.action(async () => {
|
||||
try {
|
||||
const projects = await projectManager.getAllProjects();
|
||||
|
||||
if (projects.length === 0) {
|
||||
console.log(chalk.yellow('No projects found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const tableData = [
|
||||
['Name', 'Path', 'Tech Stack', 'Sessions', 'Tokens', 'Last Active']
|
||||
];
|
||||
|
||||
for (const project of projects) {
|
||||
tableData.push([
|
||||
project.name,
|
||||
project.path,
|
||||
project.techStack.slice(0, 3).join(', ') + (project.techStack.length > 3 ? '...' : ''),
|
||||
project.totalSessions.toString(),
|
||||
project.totalTokensUsed.toLocaleString(),
|
||||
new Date(project.lastActive).toLocaleDateString()
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('\\nProjects:'));
|
||||
console.log(table(tableData));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error listing projects:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
projectCmd
|
||||
.command('info')
|
||||
.description('Show detailed project information')
|
||||
.argument('<projectId>', 'Project ID or name')
|
||||
.action(async (projectIdOrName) => {
|
||||
try {
|
||||
let project;
|
||||
|
||||
// Try to find by ID first, then by name
|
||||
project = await projectManager.getProject(projectIdOrName);
|
||||
if (!project) {
|
||||
const projects = await projectManager.getAllProjects();
|
||||
project = projects.find(p => p.name.toLowerCase() === projectIdOrName.toLowerCase());
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
console.log(chalk.yellow('Project not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\\nProject: ${project.name}`));
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
console.log(`Path: ${project.path}`);
|
||||
console.log(`Created: ${new Date(project.createdAt).toLocaleDateString()}`);
|
||||
console.log(`Last Active: ${new Date(project.lastActive).toLocaleDateString()}`);
|
||||
console.log(`Total Sessions: ${project.totalSessions}`);
|
||||
console.log(`Total Tokens: ${project.totalTokensUsed.toLocaleString()}`);
|
||||
|
||||
console.log(chalk.cyan('\\nTech Stack:'));
|
||||
project.techStack.forEach(tech => console.log(` • ${tech}`));
|
||||
|
||||
console.log(chalk.cyan('\\nCoding Style:'));
|
||||
console.log(` • Naming: ${project.codingStyle.namingConvention}`);
|
||||
console.log(` • Structure: ${project.codingStyle.fileStructure}`);
|
||||
project.codingStyle.conventions.forEach(conv => console.log(` • ${conv}`));
|
||||
|
||||
console.log(chalk.cyan('\\nCommon Commands:'));
|
||||
project.commonCommands.forEach(cmd => console.log(` • ${cmd}`));
|
||||
|
||||
console.log(chalk.cyan('\\nArchitecture:'));
|
||||
console.log(` ${project.architecture}`);
|
||||
|
||||
if (project.claudeMdContent) {
|
||||
console.log(chalk.green('\\n✓ CLAUDE.md file exists'));
|
||||
} else {
|
||||
console.log(chalk.yellow('\\n⚠ CLAUDE.md file not found. Generate with: csm project claude-md <projectId>'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error getting project info:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
projectCmd
|
||||
.command('claude-md')
|
||||
.description('Generate or update CLAUDE.md file')
|
||||
.argument('<projectId>', 'Project ID or name')
|
||||
.action(async (projectIdOrName) => {
|
||||
try {
|
||||
let project;
|
||||
|
||||
project = await projectManager.getProject(projectIdOrName);
|
||||
if (!project) {
|
||||
const projects = await projectManager.getAllProjects();
|
||||
project = projects.find(p => p.name.toLowerCase() === projectIdOrName.toLowerCase());
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
console.log(chalk.yellow('Project not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await projectManager.generateClaudeMd(project.id);
|
||||
|
||||
console.log(chalk.green('✓ CLAUDE.md generated successfully!'));
|
||||
console.log(chalk.cyan(`File: ${join(project.path, 'CLAUDE.md')}`));
|
||||
|
||||
// Show preview
|
||||
const lines = content.split('\\n');
|
||||
const preview = lines.slice(0, 20).join('\\n');
|
||||
console.log(chalk.cyan('\\nPreview:'));
|
||||
console.log(preview);
|
||||
|
||||
if (lines.length > 20) {
|
||||
console.log(chalk.gray(`... and ${lines.length - 20} more lines`));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error generating CLAUDE.md:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
projectCmd
|
||||
.command('notes')
|
||||
.description('Show session summaries for a project')
|
||||
.argument('<projectId>', 'Project ID or name')
|
||||
.option('-l, --limit <limit>', 'Number of recent summaries to show', '5')
|
||||
.action(async (projectIdOrName, options) => {
|
||||
try {
|
||||
let project;
|
||||
|
||||
project = await projectManager.getProject(projectIdOrName);
|
||||
if (!project) {
|
||||
const projects = await projectManager.getAllProjects();
|
||||
project = projects.find(p => p.name.toLowerCase() === projectIdOrName.toLowerCase());
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
console.log(chalk.yellow('Project not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const summaries = await noteTaker.getProjectSessionSummaries(project.id, parseInt(options.limit));
|
||||
|
||||
if (summaries.length === 0) {
|
||||
console.log(chalk.yellow('No session summaries found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\\nRecent Session Summaries for ${project.name}:`));
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
|
||||
summaries.forEach((summary, index) => {
|
||||
console.log(chalk.yellow(`\\n${index + 1}. ${summary.split('\\n')[0]}`));
|
||||
const preview = summary.split('\\n').slice(1, 8).join('\\n');
|
||||
console.log(preview);
|
||||
if (summary.split('\\n').length > 8) {
|
||||
console.log(chalk.gray('...'));
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error getting notes:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
projectCmd
|
||||
.command('export')
|
||||
.description('Export project session history')
|
||||
.argument('<projectId>', 'Project ID or name')
|
||||
.option('-f, --format <format>', 'Export format (markdown|json)', 'markdown')
|
||||
.option('-o, --output <file>', 'Output file path')
|
||||
.action(async (projectIdOrName, options) => {
|
||||
try {
|
||||
let project;
|
||||
|
||||
project = await projectManager.getProject(projectIdOrName);
|
||||
if (!project) {
|
||||
const projects = await projectManager.getAllProjects();
|
||||
project = projects.find(p => p.name.toLowerCase() === projectIdOrName.toLowerCase());
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
console.log(chalk.yellow('Project not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const history = await noteTaker.exportSessionHistory(project.id, options.format);
|
||||
|
||||
let outputPath = options.output;
|
||||
if (!outputPath) {
|
||||
const timestamp = new Date().toISOString().split('T')[0];
|
||||
const extension = options.format === 'json' ? 'json' : 'md';
|
||||
outputPath = join(project.path, `session-history-${timestamp}.${extension}`);
|
||||
}
|
||||
|
||||
writeFileSync(outputPath, history);
|
||||
|
||||
console.log(chalk.green('✓ Session history exported successfully!'));
|
||||
console.log(chalk.cyan(`File: ${outputPath}`));
|
||||
console.log(chalk.cyan(`Format: ${options.format}`));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error exporting history:'), error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { Database } from '../../database/Database';
|
||||
import { SessionManager } from '../../services/SessionManager';
|
||||
import { TokenTracker } from '../../services/TokenTracker';
|
||||
import { NoteTaker } from '../../services/NoteTaker';
|
||||
import inquirer from 'inquirer';
|
||||
import { table } from 'table';
|
||||
import moment from 'moment';
|
||||
|
||||
export function sessionCommands(program: Command): void {
|
||||
const db = new Database();
|
||||
const sessionManager = new SessionManager(db);
|
||||
const tokenTracker = new TokenTracker(db);
|
||||
const noteTaker = new NoteTaker(db);
|
||||
|
||||
const sessionCmd = program.command('session').description('Manage Claude sessions');
|
||||
|
||||
sessionCmd
|
||||
.command('start')
|
||||
.description('Start a new session')
|
||||
.option('-p, --project <projectId>', 'Project ID')
|
||||
.option('-m, --model <model>', 'Model type (opus|sonnet)', 'sonnet')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
let projectId = options.project;
|
||||
|
||||
if (!projectId) {
|
||||
const projects = await db.all('SELECT * FROM projects ORDER BY lastActive DESC LIMIT 5');
|
||||
if (projects.length === 0) {
|
||||
console.log(chalk.yellow('No projects found. Create a project first with: csm project create'));
|
||||
return;
|
||||
}
|
||||
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'projectId',
|
||||
message: 'Select a project:',
|
||||
choices: (projects as Array<{ name: string; path: string; id: string }>).map(p => ({
|
||||
name: `${p.name} (${p.path})`,
|
||||
value: p.id
|
||||
}))
|
||||
}
|
||||
]);
|
||||
projectId = answers.projectId;
|
||||
}
|
||||
|
||||
const session = await sessionManager.createSession(projectId, options.model);
|
||||
const project = await db.get('SELECT * FROM projects WHERE id = ?', [projectId]);
|
||||
|
||||
console.log(chalk.green('✓ Session started successfully!'));
|
||||
console.log(chalk.cyan(`Session ID: ${session.id}`));
|
||||
console.log(chalk.cyan(`Project: ${(project as { name: string })?.name}`));
|
||||
console.log(chalk.cyan(`Model: ${session.modelType}`));
|
||||
console.log(chalk.cyan(`Started: ${moment(session.startTime).format('YYYY-MM-DD HH:mm:ss')}`));
|
||||
|
||||
console.log(chalk.yellow('\\nTips for this session:'));
|
||||
console.log('• Use /clear frequently to reset conversation history');
|
||||
console.log('• Batch similar requests into single prompts');
|
||||
console.log('• Use /compact when conversation gets long but context is needed');
|
||||
console.log('• Track tokens with: csm session track <sessionId>');
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error starting session:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
sessionCmd
|
||||
.command('end')
|
||||
.description('End the current session')
|
||||
.option('-i, --id <sessionId>', 'Session ID (if not provided, ends active session)')
|
||||
.option('-s, --summary <summary>', 'Session summary')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
let sessionId = options.id;
|
||||
|
||||
if (!sessionId) {
|
||||
const activeSession = await sessionManager.getActiveSession();
|
||||
if (!activeSession) {
|
||||
console.log(chalk.yellow('No active session found.'));
|
||||
return;
|
||||
}
|
||||
sessionId = activeSession.id;
|
||||
}
|
||||
|
||||
const summary = options.summary || await noteTaker.generateSessionSummary(sessionId);
|
||||
const session = await sessionManager.endSession(sessionId, summary);
|
||||
|
||||
console.log(chalk.green('✓ Session ended successfully!'));
|
||||
console.log(chalk.cyan(`Duration: ${moment.duration(session.endTime!.getTime() - session.startTime.getTime()).humanize()}`));
|
||||
console.log(chalk.cyan(`Tokens used: ${session.tokensUsed.toLocaleString()}`));
|
||||
console.log(chalk.cyan(`Messages: ${session.messages}`));
|
||||
|
||||
const optimizationScore = await tokenTracker.calculateOptimizationScore(session);
|
||||
console.log(chalk.cyan(`Optimization score: ${optimizationScore}/100`));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error ending session:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
sessionCmd
|
||||
.command('track')
|
||||
.description('Track token usage for a session')
|
||||
.argument('<sessionId>', 'Session ID')
|
||||
.argument('<tokens>', 'Tokens used')
|
||||
.argument('<messages>', 'Number of messages')
|
||||
.action(async (sessionId, tokens, messages) => {
|
||||
try {
|
||||
await sessionManager.updateSessionTokens(sessionId, parseInt(tokens), parseInt(messages));
|
||||
await tokenTracker.trackTokenUsage(sessionId, parseInt(tokens));
|
||||
|
||||
console.log(chalk.green('✓ Token usage tracked successfully!'));
|
||||
console.log(chalk.cyan(`Tokens: ${parseInt(tokens).toLocaleString()}`));
|
||||
console.log(chalk.cyan(`Messages: ${messages}`));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error tracking usage:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
sessionCmd
|
||||
.command('note')
|
||||
.description('Add a note to the current session')
|
||||
.argument('<note>', 'Note content')
|
||||
.option('-t, --type <type>', 'Note type (bug|feature|question|idea)', 'general')
|
||||
.option('-i, --id <sessionId>', 'Session ID (if not provided, uses active session)')
|
||||
.action(async (note, options) => {
|
||||
try {
|
||||
let sessionId = options.id;
|
||||
|
||||
if (!sessionId) {
|
||||
const activeSession = await sessionManager.getActiveSession();
|
||||
if (!activeSession) {
|
||||
console.log(chalk.yellow('No active session found.'));
|
||||
return;
|
||||
}
|
||||
sessionId = activeSession.id;
|
||||
}
|
||||
|
||||
if (options.type !== 'general') {
|
||||
await noteTaker.addQuickNote(sessionId, options.type, note);
|
||||
} else {
|
||||
await noteTaker.takeSessionNote(sessionId, note);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✓ Note added successfully!'));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error adding note:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
sessionCmd
|
||||
.command('list')
|
||||
.description('List recent sessions')
|
||||
.option('-l, --limit <limit>', 'Number of sessions to show', '10')
|
||||
.option('-p, --project <projectId>', 'Filter by project')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
let sessions;
|
||||
if (options.project) {
|
||||
sessions = await db.all(
|
||||
'SELECT * FROM sessions WHERE projectId = ? ORDER BY startTime DESC LIMIT ?',
|
||||
[options.project, parseInt(options.limit)]
|
||||
);
|
||||
} else {
|
||||
sessions = await sessionManager.getSessionHistory(parseInt(options.limit));
|
||||
}
|
||||
|
||||
if (sessions.length === 0) {
|
||||
console.log(chalk.yellow('No sessions found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const tableData = [
|
||||
['ID', 'Project', 'Model', 'Tokens', 'Duration', 'Status']
|
||||
];
|
||||
|
||||
for (const session of sessions) {
|
||||
const project = await db.get('SELECT name FROM projects WHERE id = ?', [session.projectId]);
|
||||
const duration = session.endTime
|
||||
? moment.duration(session.endTime - session.startTime).humanize()
|
||||
: 'Active';
|
||||
|
||||
tableData.push([
|
||||
session.id.substring(0, 8),
|
||||
(project as { name: string })?.name || 'Unknown',
|
||||
session.modelType,
|
||||
session.tokensUsed.toLocaleString(),
|
||||
duration,
|
||||
session.status
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('\\nRecent Sessions:'));
|
||||
console.log(table(tableData));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error listing sessions:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
sessionCmd
|
||||
.command('status')
|
||||
.description('Show current session and window status')
|
||||
.action(async () => {
|
||||
try {
|
||||
const activeSession = await sessionManager.getActiveSession();
|
||||
const currentWindow = await sessionManager.getCurrentWindow();
|
||||
const nextWindow = await sessionManager.getNextAvailableWindow();
|
||||
|
||||
console.log(chalk.cyan('\\nSession Status:'));
|
||||
|
||||
if (activeSession) {
|
||||
const project = await db.get('SELECT name FROM projects WHERE id = ?', [activeSession.projectId]);
|
||||
console.log(chalk.green('✓ Active Session:'));
|
||||
console.log(` ID: ${activeSession.id.substring(0, 8)}`);
|
||||
console.log(` Project: ${(project as { name: string })?.name}`);
|
||||
console.log(` Model: ${activeSession.modelType}`);
|
||||
console.log(` Started: ${moment(activeSession.startTime).format('HH:mm:ss')}`);
|
||||
console.log(` Tokens: ${activeSession.tokensUsed.toLocaleString()}`);
|
||||
} else {
|
||||
console.log(chalk.yellow('No active session'));
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('\\nWindow Status:'));
|
||||
|
||||
if (currentWindow) {
|
||||
console.log(chalk.green('✓ In Active Window:'));
|
||||
console.log(` Started: ${moment(currentWindow.startTime).format('HH:mm:ss')}`);
|
||||
console.log(` Ends: ${moment(currentWindow.endTime).format('HH:mm:ss')}`);
|
||||
console.log(` Remaining: ${moment.duration(currentWindow.endTime.getTime() - Date.now()).humanize()}`);
|
||||
} else if (nextWindow) {
|
||||
console.log(chalk.yellow('Next window available:'));
|
||||
console.log(` Starts: ${moment(nextWindow).format('YYYY-MM-DD HH:mm:ss')}`);
|
||||
console.log(` In: ${moment.duration(nextWindow.getTime() - Date.now()).humanize()}`);
|
||||
} else {
|
||||
console.log(chalk.yellow('No windows scheduled'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error checking status:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
sessionCmd
|
||||
.command('overlap')
|
||||
.description('Schedule overlap hack for extended sessions')
|
||||
.option('-d, --delay <hours>', 'Delay in hours before starting overlap', '2')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const activeSession = await sessionManager.getActiveSession();
|
||||
if (!activeSession) {
|
||||
console.log(chalk.yellow('No active session found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const schedule = await sessionManager.scheduleOverlapHack(activeSession.id, parseInt(options.delay));
|
||||
|
||||
console.log(chalk.green('✓ Overlap hack scheduled!'));
|
||||
console.log(chalk.cyan(`Current session: ${activeSession.id.substring(0, 8)}`));
|
||||
console.log(chalk.cyan(`Overlap starts: ${moment(schedule.startTime).format('YYYY-MM-DD HH:mm:ss')}`));
|
||||
console.log(chalk.cyan(`Overlap ends: ${moment(schedule.endTime).format('YYYY-MM-DD HH:mm:ss')}`));
|
||||
|
||||
console.log(chalk.yellow('\\nThis will give you a fresh 5-hour window while maintaining context!'));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error scheduling overlap:'), error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { Database } from '../../database/Database';
|
||||
import { TokenTracker } from '../../services/TokenTracker';
|
||||
import { WorkflowOptimizer } from '../../services/WorkflowOptimizer';
|
||||
import { SessionManager } from '../../services/SessionManager';
|
||||
import { table } from 'table';
|
||||
import moment from 'moment';
|
||||
|
||||
export function statsCommands(program: Command): void {
|
||||
const db = new Database();
|
||||
const tokenTracker = new TokenTracker(db);
|
||||
const sessionManager = new SessionManager(db);
|
||||
const workflowOptimizer = new WorkflowOptimizer(db, tokenTracker, sessionManager);
|
||||
|
||||
const statsCmd = program.command('stats').description('View usage statistics and analytics');
|
||||
|
||||
statsCmd
|
||||
.command('usage')
|
||||
.description('Show token usage statistics')
|
||||
.option('-d, --days <days>', 'Number of days to analyze', '7')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const usageStats = await tokenTracker.getUsageStats(parseInt(options.days));
|
||||
|
||||
if (usageStats.length === 0) {
|
||||
console.log(chalk.yellow('No usage data found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const tableData = [
|
||||
['Date', 'Sessions', 'Tokens', 'Opus', 'Sonnet', 'Opt Score']
|
||||
];
|
||||
|
||||
let totalTokens = 0;
|
||||
let totalSessions = 0;
|
||||
|
||||
for (const stats of usageStats) {
|
||||
tableData.push([
|
||||
moment(stats.date).format('MM/DD'),
|
||||
stats.sessionsCount.toString(),
|
||||
stats.totalTokens.toLocaleString(),
|
||||
stats.opusSessions.toString(),
|
||||
stats.sonnetSessions.toString(),
|
||||
`${stats.optimizationScore}/100`
|
||||
]);
|
||||
|
||||
totalTokens += stats.totalTokens;
|
||||
totalSessions += stats.sessionsCount;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\\nUsage Statistics (${options.days} days):`));
|
||||
console.log(table(tableData));
|
||||
|
||||
console.log(chalk.cyan('\\nSummary:'));
|
||||
console.log(`Total Sessions: ${totalSessions}`);
|
||||
console.log(`Total Tokens: ${totalTokens.toLocaleString()}`);
|
||||
console.log(`Avg Tokens/Session: ${Math.round(totalTokens / totalSessions).toLocaleString()}`);
|
||||
|
||||
// Show efficiency trend
|
||||
const trend = await tokenTracker.getTokenEfficiencyTrend(parseInt(options.days));
|
||||
if (trend.length > 1) {
|
||||
const recent = trend.slice(-3);
|
||||
const avgEfficiency = recent.reduce((sum, day) => sum + day.efficiency, 0) / recent.length;
|
||||
console.log(`Avg Efficiency: ${Math.round(avgEfficiency)}%`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error getting usage stats:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
statsCmd
|
||||
.command('today')
|
||||
.description('Show today\'s usage and recommendations')
|
||||
.action(async () => {
|
||||
try {
|
||||
const todayUsage = await tokenTracker.getTodayUsage();
|
||||
const recommendations = await tokenTracker.getOptimizationRecommendations();
|
||||
|
||||
console.log(chalk.cyan('\\nToday\'s Usage:'));
|
||||
|
||||
if (todayUsage) {
|
||||
console.log(`Sessions: ${todayUsage.sessionsCount}`);
|
||||
console.log(`Tokens Used: ${todayUsage.totalTokens.toLocaleString()}`);
|
||||
console.log(`Opus Sessions: ${todayUsage.opusSessions}`);
|
||||
console.log(`Sonnet Sessions: ${todayUsage.sonnetSessions}`);
|
||||
console.log(`Optimization Score: ${todayUsage.optimizationScore}/100`);
|
||||
} else {
|
||||
console.log(chalk.yellow('No sessions today.'));
|
||||
}
|
||||
|
||||
if (recommendations.length > 0) {
|
||||
console.log(chalk.cyan('\\nRecommendations:'));
|
||||
recommendations.forEach((rec, index) => {
|
||||
const priorityColor = rec.priority === 'high' ? chalk.red :
|
||||
rec.priority === 'medium' ? chalk.yellow : chalk.green;
|
||||
console.log(`${index + 1}. ${priorityColor(`[${rec.priority.toUpperCase()}]`)} ${rec.message}`);
|
||||
if (rec.potentialSavings) {
|
||||
console.log(` Potential savings: ${rec.potentialSavings.toLocaleString()} tokens`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.green('\\n✓ Great job! No optimization recommendations at this time.'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error getting today\'s stats:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
statsCmd
|
||||
.command('optimize')
|
||||
.description('Analyze workflow efficiency and provide insights')
|
||||
.option('-p, --project <projectId>', 'Analyze specific project')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const analysis = await workflowOptimizer.analyzeWorkflowEfficiency(options.project);
|
||||
|
||||
console.log(chalk.cyan('\\nWorkflow Efficiency Analysis:'));
|
||||
console.log(chalk.gray('─'.repeat(40)));
|
||||
|
||||
// Score visualization
|
||||
const score = analysis.score;
|
||||
const scoreColor = score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
|
||||
console.log(`\\nEfficiency Score: ${scoreColor(`${score}/100`)}`);
|
||||
|
||||
// Progress bar
|
||||
const barLength = 20;
|
||||
const filledLength = Math.round((score / 100) * barLength);
|
||||
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
|
||||
console.log(`[${scoreColor(bar)}]`);
|
||||
|
||||
if (analysis.recommendations.length > 0) {
|
||||
console.log(chalk.cyan('\\nTop Recommendations:'));
|
||||
analysis.recommendations.slice(0, 5).forEach((rec, index) => {
|
||||
const priorityColor = rec.priority === 'high' ? chalk.red :
|
||||
rec.priority === 'medium' ? chalk.yellow : chalk.green;
|
||||
console.log(`${index + 1}. ${priorityColor(`[${rec.priority.toUpperCase()}]`)} ${rec.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (analysis.insights.length > 0) {
|
||||
console.log(chalk.cyan('\\nKey Insights:'));
|
||||
analysis.insights.forEach(insight => {
|
||||
console.log(`• ${insight}`);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error analyzing workflow:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
statsCmd
|
||||
.command('anomalies')
|
||||
.description('Detect workflow anomalies and trends')
|
||||
.action(async () => {
|
||||
try {
|
||||
const analysis = await workflowOptimizer.detectWorkflowAnomalies();
|
||||
|
||||
console.log(chalk.cyan('\\nWorkflow Anomaly Detection:'));
|
||||
console.log(chalk.gray('─'.repeat(40)));
|
||||
|
||||
if (analysis.anomalies.length > 0) {
|
||||
console.log(chalk.yellow('\\n⚠ Anomalies Detected:'));
|
||||
analysis.anomalies.forEach((anomaly, index) => {
|
||||
const severityColor = anomaly.severity === 'high' ? chalk.red :
|
||||
anomaly.severity === 'medium' ? chalk.yellow : chalk.green;
|
||||
console.log(`\\n${index + 1}. ${severityColor(`[${anomaly.severity.toUpperCase()}]`)} ${anomaly.type.replace('_', ' ').toUpperCase()}`);
|
||||
console.log(` ${anomaly.description}`);
|
||||
console.log(chalk.cyan(` Recommendation: ${anomaly.recommendation}`));
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.green('\\n✓ No anomalies detected. Your workflow looks healthy!'));
|
||||
}
|
||||
|
||||
if (analysis.trends.length > 0) {
|
||||
console.log(chalk.cyan('\\n📈 Trends:'));
|
||||
analysis.trends.forEach(trend => {
|
||||
const trendColor = trend.trend === 'improving' ? chalk.green :
|
||||
trend.trend === 'declining' ? chalk.red : chalk.gray;
|
||||
const icon = trend.trend === 'improving' ? '↑' :
|
||||
trend.trend === 'declining' ? '↓' : '→';
|
||||
console.log(` ${trendColor(`${icon} ${trend.metric.replace('_', ' ')}: ${trend.trend} (${trend.change > 0 ? '+' : ''}${trend.change})`)}`);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error detecting anomalies:'), error.message);
|
||||
}
|
||||
});
|
||||
|
||||
statsCmd
|
||||
.command('tokens')
|
||||
.description('Detailed token usage breakdown')
|
||||
.option('-p, --project <projectId>', 'Breakdown by project')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
if (options.project) {
|
||||
// Project-specific breakdown
|
||||
const breakdown = await tokenTracker.projectTokenBreakdown(options.project);
|
||||
|
||||
console.log(chalk.cyan('\\nToken Breakdown by Project:'));
|
||||
console.log(`Total Tokens: ${breakdown.total.toLocaleString()}`);
|
||||
console.log(`Opus: ${breakdown.byModel.opus.toLocaleString()} (${Math.round(breakdown.byModel.opus / breakdown.total * 100)}%)`);
|
||||
console.log(`Sonnet: ${breakdown.byModel.sonnet.toLocaleString()} (${Math.round(breakdown.byModel.sonnet / breakdown.total * 100)}%)`);
|
||||
} else {
|
||||
// Overall breakdown with model efficiency
|
||||
const usageStats = await tokenTracker.getUsageStats(30);
|
||||
|
||||
let totalOpus = 0;
|
||||
let totalSonnet = 0;
|
||||
let opusSessions = 0;
|
||||
let sonnetSessions = 0;
|
||||
|
||||
usageStats.forEach(stats => {
|
||||
totalOpus += stats.totalTokens * (stats.opusSessions / Math.max(1, stats.sessionsCount));
|
||||
totalSonnet += stats.totalTokens * (stats.sonnetSessions / Math.max(1, stats.sessionsCount));
|
||||
opusSessions += stats.opusSessions;
|
||||
sonnetSessions += stats.sonnetSessions;
|
||||
});
|
||||
|
||||
const total = totalOpus + totalSonnet;
|
||||
|
||||
console.log(chalk.cyan('\\n30-Day Token Breakdown:'));
|
||||
console.log(`Total Tokens: ${total.toLocaleString()}`);
|
||||
console.log(`Opus: ${totalOpus.toLocaleString()} (${Math.round(totalOpus / total * 100)}%) - ${opusSessions} sessions`);
|
||||
console.log(`Sonnet: ${totalSonnet.toLocaleString()} (${Math.round(totalSonnet / total * 100)}%) - ${sonnetSessions} sessions`);
|
||||
|
||||
// Efficiency comparison
|
||||
const opusAvg = opusSessions > 0 ? totalOpus / opusSessions : 0;
|
||||
const sonnetAvg = sonnetSessions > 0 ? totalSonnet / sonnetSessions : 0;
|
||||
|
||||
console.log(chalk.cyan('\\nAverage Tokens per Session:'));
|
||||
console.log(`Opus: ${Math.round(opusAvg).toLocaleString()}`);
|
||||
console.log(`Sonnet: ${Math.round(sonnetAvg).toLocaleString()}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error getting token breakdown:'), error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
export function log(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info'): void {
|
||||
const colors = {
|
||||
info: chalk.blue,
|
||||
success: chalk.green,
|
||||
warning: chalk.yellow,
|
||||
error: chalk.red
|
||||
};
|
||||
|
||||
console.log(colors[type](`[${type.toUpperCase()}] ${message}`));
|
||||
}
|
||||
|
||||
export function success(message: string): void {
|
||||
log(message, 'success');
|
||||
}
|
||||
|
||||
export function error(message: string | any): void {
|
||||
const msg = typeof message === 'string' ? message : message?.message || String(message);
|
||||
log(msg, 'error');
|
||||
}
|
||||
|
||||
export function warning(message: string): void {
|
||||
log(message, 'warning');
|
||||
}
|
||||
|
||||
export function info(message: string): void {
|
||||
log(message, 'info');
|
||||
}
|
||||
|
||||
export function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTokens(tokens: number): string {
|
||||
if (tokens >= 1000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`;
|
||||
} else if (tokens >= 1000) {
|
||||
return `${(tokens / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return tokens.toString();
|
||||
}
|
||||
|
||||
export function truncateString(str: string, maxLength: number): string {
|
||||
if (str.length <= maxLength) return str;
|
||||
return str.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Database as SQLiteDatabase } from 'bun:sqlite';
|
||||
|
||||
export class DatabaseManager {
|
||||
private db: SQLiteDatabase;
|
||||
|
||||
constructor(dbPath: string = './data/claude-sessions.db') {
|
||||
// Create database (bun:sqlite automatically creates directories if needed)
|
||||
this.db = new SQLiteDatabase(dbPath);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
techStack TEXT,
|
||||
codingStyle TEXT,
|
||||
claudeMdContent TEXT,
|
||||
commonCommands TEXT,
|
||||
architecture TEXT,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
lastActive DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
totalSessions INTEGER DEFAULT 0,
|
||||
totalTokensUsed INTEGER DEFAULT 0
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
projectId TEXT NOT NULL,
|
||||
startTime DATETIME NOT NULL,
|
||||
endTime DATETIME,
|
||||
modelType TEXT NOT NULL,
|
||||
tokensUsed INTEGER DEFAULT 0,
|
||||
messages INTEGER DEFAULT 0,
|
||||
notes TEXT,
|
||||
summary TEXT,
|
||||
status TEXT NOT NULL,
|
||||
optimizationScore INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (projectId) REFERENCES projects (id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS usage_stats (
|
||||
id TEXT PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
sessionsCount INTEGER DEFAULT 0,
|
||||
totalTokens INTEGER DEFAULT 0,
|
||||
opusSessions INTEGER DEFAULT 0,
|
||||
sonnetSessions INTEGER DEFAULT 0,
|
||||
averageSessionLength REAL DEFAULT 0,
|
||||
optimizationScore INTEGER DEFAULT 0,
|
||||
recommendations TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS window_schedules (
|
||||
id TEXT PRIMARY KEY,
|
||||
startTime DATETIME NOT NULL,
|
||||
endTime DATETIME NOT NULL,
|
||||
isActive BOOLEAN DEFAULT 0,
|
||||
overlapSession TEXT
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
run(sql: string, params: unknown[] = []): unknown {
|
||||
const stmt = this.db.prepare(sql);
|
||||
return stmt.run(...params as []);
|
||||
}
|
||||
|
||||
get(sql: string, params: unknown[] = []): unknown {
|
||||
const stmt = this.db.prepare(sql);
|
||||
return stmt.get(...params as []);
|
||||
}
|
||||
|
||||
all(sql: string, params: unknown[] = []): unknown[] {
|
||||
const stmt = this.db.prepare(sql);
|
||||
return stmt.all(...params as []);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Export as Database for backward compatibility
|
||||
export { DatabaseManager as Database };
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { Command } from "commander";
|
||||
import chalk from "chalk";
|
||||
import { Database } from "./database/Database";
|
||||
import { sessionCommands } from "./cli/commands/session";
|
||||
import { projectCommands } from "./cli/commands/project";
|
||||
import { statsCommands } from "./cli/commands/stats";
|
||||
import { optimizeCommands } from "./cli/commands/optimize";
|
||||
import { info, success, error } from "./cli/utils/logger";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("csm")
|
||||
.description("Claude Session Manager - Optimize your Claude Code sessions")
|
||||
.version("1.0.0");
|
||||
|
||||
// Initialize database
|
||||
let db: Database;
|
||||
|
||||
try {
|
||||
db = new Database();
|
||||
success("Claude Session Manager initialized successfully!");
|
||||
} catch (err) {
|
||||
error(`Failed to initialize database: ${err}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Register command modules
|
||||
sessionCommands(program);
|
||||
projectCommands(program);
|
||||
statsCommands(program);
|
||||
optimizeCommands(program);
|
||||
|
||||
// Global options
|
||||
program
|
||||
.option("-v, --verbose", "Enable verbose logging")
|
||||
.hook("preAction", (thisCommand) => {
|
||||
if (thisCommand.opts().verbose) {
|
||||
info("Verbose mode enabled");
|
||||
}
|
||||
});
|
||||
|
||||
// Default command - show status
|
||||
program.action(() => {
|
||||
console.log(chalk.cyan("🤖 Claude Session Manager"));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
"Optimize your Claude Code sessions based on PLAN.md guidelines",
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.yellow("Quick Start:"));
|
||||
console.log(" csm project create Create a new project");
|
||||
console.log(" csm session start Start a new session");
|
||||
console.log(" csm stats today Show today's usage");
|
||||
console.log(" csm optimize tips Show optimization tips");
|
||||
|
||||
console.log(chalk.yellow("Commands:"));
|
||||
console.log(" session Manage Claude sessions");
|
||||
console.log(" project Manage projects and CLAUDE.md files");
|
||||
console.log(" stats View usage statistics and analytics");
|
||||
console.log(" optimize Get optimization suggestions");
|
||||
|
||||
console.log(chalk.yellow("Examples:"));
|
||||
console.log(" csm session start -p my-project -m sonnet");
|
||||
console.log(' csm optimize suggest "implement user authentication"');
|
||||
console.log(" csm project claude-md my-project");
|
||||
console.log(" csm stats usage --days 30");
|
||||
|
||||
console.log(
|
||||
chalk.green(
|
||||
"💡 Remember: Use /clear frequently and batch similar requests!",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Error handling
|
||||
program.on("command:*", () => {
|
||||
error(`Invalid command: ${program.args.join(" ")}`);
|
||||
console.log("See --help for a list of available commands.");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Parse command line arguments
|
||||
program.parse();
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on("SIGINT", () => {
|
||||
info("\\nShutting down Claude Session Manager...");
|
||||
if (db) {
|
||||
db.close();
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
info("\\nShutting down Claude Session Manager...");
|
||||
if (db) {
|
||||
db.close();
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface CodingStyle {
|
||||
namingConvention: 'snake_case' | 'camelCase' | 'PascalCase';
|
||||
fileStructure: string;
|
||||
conventions: string[];
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
techStack: string[];
|
||||
codingStyle: CodingStyle;
|
||||
claudeMdContent?: string;
|
||||
commonCommands: string[];
|
||||
architecture: string;
|
||||
createdAt: Date;
|
||||
lastActive: Date;
|
||||
totalSessions: number;
|
||||
totalTokensUsed: number;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface Session {
|
||||
id: string;
|
||||
projectId: string;
|
||||
startTime: Date;
|
||||
endTime?: Date;
|
||||
modelType: 'opus' | 'sonnet';
|
||||
tokensUsed: number;
|
||||
messages: number;
|
||||
notes: string;
|
||||
summary: string;
|
||||
status: 'active' | 'completed' | 'interrupted';
|
||||
optimizationScore: number;
|
||||
}
|
||||
|
||||
export interface WindowSchedule {
|
||||
id: string;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
isActive: boolean;
|
||||
overlapSession?: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export interface UsageStats {
|
||||
id: string;
|
||||
date: Date;
|
||||
sessionsCount: number;
|
||||
totalTokens: number;
|
||||
opusSessions: number;
|
||||
sonnetSessions: number;
|
||||
averageSessionLength: number;
|
||||
optimizationScore: number;
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
export interface OptimizationRecommendation {
|
||||
type: 'token' | 'scheduling' | 'workflow' | 'model';
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
message: string;
|
||||
potentialSavings?: number;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './Session';
|
||||
export * from './Project';
|
||||
export * from './UsageStats';
|
||||
@@ -0,0 +1,299 @@
|
||||
import { Database } from '../database/Database';
|
||||
import { Session, Project } from '../models';
|
||||
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import moment from 'moment';
|
||||
|
||||
export class NoteTaker {
|
||||
private db: Database;
|
||||
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async takeSessionNote(sessionId: string, note: string): Promise<void> {
|
||||
await this.db.run(
|
||||
'UPDATE sessions SET notes = notes || ? || "\\n\\n" WHERE id = ?',
|
||||
[`[${new Date().toISOString()}] ${note}`, sessionId]
|
||||
);
|
||||
}
|
||||
|
||||
async generateSessionSummary(sessionId: string): Promise<string> {
|
||||
const session = await this.getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
|
||||
const project = await this.getProject(session.projectId);
|
||||
const duration = session.endTime
|
||||
? moment.duration(session.endTime.getTime() - session.startTime.getTime()).humanize()
|
||||
: 'Still active';
|
||||
|
||||
const summary = `# Session Summary - ${moment(session.startTime).format('YYYY-MM-DD HH:mm')}
|
||||
|
||||
## Project: ${project?.name || 'Unknown'}
|
||||
**Duration**: ${duration}
|
||||
**Model**: ${session.modelType}
|
||||
**Tokens Used**: ${session.tokensUsed.toLocaleString()}
|
||||
**Messages**: ${session.messages}
|
||||
|
||||
## Key Activities
|
||||
${this.extractKeyActivities(session.notes)}
|
||||
|
||||
## Files Modified
|
||||
${this.extractFilesModified(session.notes)}
|
||||
|
||||
## Outcomes
|
||||
${this.extractOutcomes(session.notes)}
|
||||
|
||||
## Next Steps
|
||||
${this.extractNextSteps(session.notes)}
|
||||
|
||||
## Optimization Notes
|
||||
${this.generateOptimizationNotes(session)}
|
||||
|
||||
---
|
||||
*Generated by Claude Session Manager*
|
||||
`;
|
||||
|
||||
// Save summary to database
|
||||
await this.db.run(
|
||||
'UPDATE sessions SET summary = ? WHERE id = ?',
|
||||
[summary, sessionId]
|
||||
);
|
||||
|
||||
// Save to file if project exists
|
||||
if (project) {
|
||||
await this.saveSessionNotesToFile(project, session, summary);
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
async getSessionNotes(sessionId: string): Promise<string> {
|
||||
const session = await this.getSession(sessionId);
|
||||
return session?.notes || '';
|
||||
}
|
||||
|
||||
async getProjectSessionSummaries(projectId: string, limit: number = 10): Promise<string[]> {
|
||||
const sessions = await this.db.all(
|
||||
`SELECT summary FROM sessions
|
||||
WHERE projectId = ? AND summary IS NOT NULL AND summary != ''
|
||||
ORDER BY startTime DESC
|
||||
LIMIT ?`,
|
||||
[projectId, limit]
|
||||
);
|
||||
|
||||
return (sessions as Array<{ summary: string }>).map(session => session.summary);
|
||||
}
|
||||
|
||||
async addQuickNote(sessionId: string, type: 'bug' | 'feature' | 'question' | 'idea', content: string): Promise<void> {
|
||||
const timestamp = moment().format('HH:mm');
|
||||
const formattedNote = `[${timestamp}] [${type.toUpperCase()}] ${content}`;
|
||||
|
||||
await this.takeSessionNote(sessionId, formattedNote);
|
||||
}
|
||||
|
||||
async searchSessionNotes(projectId: string, query: string): Promise<{sessionId: string, note: string, timestamp: Date}[]> {
|
||||
const sessions = await this.db.all(
|
||||
`SELECT id, notes, startTime FROM sessions
|
||||
WHERE projectId = ? AND notes LIKE ?
|
||||
ORDER BY startTime DESC`,
|
||||
[projectId, `%${query}%`]
|
||||
);
|
||||
|
||||
return (sessions as Array<{ id: string; notes: string; startTime: string }>).map(session => ({
|
||||
sessionId: session.id,
|
||||
note: session.notes,
|
||||
timestamp: new Date(session.startTime)
|
||||
}));
|
||||
}
|
||||
|
||||
async exportSessionHistory(projectId: string, format: 'markdown' | 'json' = 'markdown'): Promise<string> {
|
||||
const sessions = await this.db.all(
|
||||
`SELECT * FROM sessions
|
||||
WHERE projectId = ?
|
||||
ORDER BY startTime DESC`,
|
||||
[projectId]
|
||||
);
|
||||
|
||||
if (format === 'json') {
|
||||
return JSON.stringify(sessions, null, 2);
|
||||
}
|
||||
|
||||
// Markdown format
|
||||
let markdown = `# Session History\n\n`;
|
||||
|
||||
for (const session of sessions as Array<{
|
||||
startTime: string;
|
||||
modelType: string;
|
||||
tokensUsed: number;
|
||||
status: string;
|
||||
notes?: string;
|
||||
summary?: string;
|
||||
}>) {
|
||||
markdown += `## Session - ${moment(session.startTime).format('YYYY-MM-DD HH:mm')}\n\n`;
|
||||
markdown += `**Model**: ${session.modelType}\n`;
|
||||
markdown += `**Tokens**: ${session.tokensUsed.toLocaleString()}\n`;
|
||||
markdown += `**Status**: ${session.status}\n\n`;
|
||||
|
||||
if (session.notes) {
|
||||
markdown += `### Notes\n${session.notes}\n\n`;
|
||||
}
|
||||
|
||||
if (session.summary) {
|
||||
markdown += `### Summary\n${session.summary}\n\n`;
|
||||
}
|
||||
|
||||
markdown += `---\n\n`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
private async getSession(sessionId: string): Promise<Session | null> {
|
||||
const row = await this.db.get('SELECT * FROM sessions WHERE id = ?', [sessionId]);
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
id: (row as { id: string }).id,
|
||||
projectId: (row as { projectId: string }).projectId,
|
||||
startTime: new Date((row as { startTime: string }).startTime),
|
||||
endTime: (row as { endTime?: string }).endTime ? new Date((row as { endTime: string }).endTime) : undefined,
|
||||
modelType: (row as { modelType: 'opus' | 'sonnet' }).modelType,
|
||||
tokensUsed: (row as { tokensUsed: number }).tokensUsed,
|
||||
messages: (row as { messages: number }).messages,
|
||||
notes: (row as { notes: string }).notes,
|
||||
summary: (row as { summary: string }).summary,
|
||||
status: (row as { status: 'active' | 'completed' | 'interrupted' }).status,
|
||||
optimizationScore: (row as { optimizationScore: number }).optimizationScore
|
||||
};
|
||||
}
|
||||
|
||||
private async getProject(projectId: string): Promise<Project | null> {
|
||||
const row = await this.db.get('SELECT * FROM projects WHERE id = ?', [projectId]);
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
id: (row as { id: string }).id,
|
||||
name: (row as { name: string }).name,
|
||||
path: (row as { path: string }).path,
|
||||
techStack: JSON.parse((row as { techStack: string }).techStack || '[]'),
|
||||
codingStyle: JSON.parse((row as { codingStyle: string }).codingStyle || '{}'),
|
||||
claudeMdContent: (row as { claudeMdContent: string }).claudeMdContent,
|
||||
commonCommands: JSON.parse((row as { commonCommands: string }).commonCommands || '[]'),
|
||||
architecture: (row as { architecture: string }).architecture,
|
||||
createdAt: new Date((row as { createdAt: string }).createdAt),
|
||||
lastActive: new Date((row as { lastActive: string }).lastActive),
|
||||
totalSessions: (row as { totalSessions: number }).totalSessions,
|
||||
totalTokensUsed: (row as { totalTokensUsed: number }).totalTokensUsed
|
||||
};
|
||||
}
|
||||
|
||||
private extractKeyActivities(notes: string): string {
|
||||
// Extract activities from notes - this is a simplified implementation
|
||||
const lines = notes.split('\n').filter(line =>
|
||||
line.includes('implemented') ||
|
||||
line.includes('fixed') ||
|
||||
line.includes('created') ||
|
||||
line.includes('updated')
|
||||
);
|
||||
|
||||
return lines.length > 0
|
||||
? lines.map(line => `- ${line.trim()}`).join('\n')
|
||||
: '- No specific activities recorded';
|
||||
}
|
||||
|
||||
private extractFilesModified(notes: string): string {
|
||||
// Extract file mentions from notes
|
||||
const filePattern = /\b[\w\-/]+\.(js|ts|jsx|tsx|py|java|go|rs|cpp|c|html|css|json|md)\b/g;
|
||||
const matches = notes.match(filePattern) || [];
|
||||
const uniqueFiles = [...new Set(matches)];
|
||||
|
||||
return uniqueFiles.length > 0
|
||||
? uniqueFiles.map(file => `- ${file}`).join('\n')
|
||||
: '- No files specifically mentioned';
|
||||
}
|
||||
|
||||
private extractOutcomes(notes: string): string {
|
||||
// Extract outcomes from notes
|
||||
const outcomeKeywords = ['completed', 'finished', 'resolved', 'achieved', 'success'];
|
||||
const lines = notes.split('\n').filter(line =>
|
||||
outcomeKeywords.some(keyword => line.toLowerCase().includes(keyword))
|
||||
);
|
||||
|
||||
return lines.length > 0
|
||||
? lines.map(line => `- ${line.trim()}`).join('\n')
|
||||
: '- Outcomes not explicitly recorded';
|
||||
}
|
||||
|
||||
private extractNextSteps(notes: string): string {
|
||||
// Extract next steps from notes
|
||||
const nextStepKeywords = ['next', 'todo', 'follow-up', 'later', 'remaining'];
|
||||
const lines = notes.split('\n').filter(line =>
|
||||
nextStepKeywords.some(keyword => line.toLowerCase().includes(keyword))
|
||||
);
|
||||
|
||||
return lines.length > 0
|
||||
? lines.map(line => `- ${line.trim()}`).join('\n')
|
||||
: '- No next steps recorded';
|
||||
}
|
||||
|
||||
private generateOptimizationNotes(session: Session): string {
|
||||
const notes = [];
|
||||
|
||||
if (session.tokensUsed > 50000) {
|
||||
notes.push('- High token usage detected. Consider using /compact more frequently');
|
||||
}
|
||||
|
||||
if (session.messages < 5) {
|
||||
notes.push('- Low message count may indicate inefficient communication');
|
||||
}
|
||||
|
||||
if (session.modelType === 'opus' && session.tokensUsed < 10000) {
|
||||
notes.push('- Opus used for light task. Consider using Sonnet for better efficiency');
|
||||
}
|
||||
|
||||
return notes.length > 0 ? notes.join('\n') : '- Good optimization practices observed';
|
||||
}
|
||||
|
||||
private async saveSessionNotesToFile(project: Project, session: Session, summary: string): Promise<void> {
|
||||
try {
|
||||
const notesDir = join(project.path, '.claude-sessions');
|
||||
if (!existsSync(notesDir)) {
|
||||
mkdirSync(notesDir, { recursive: true });
|
||||
}
|
||||
|
||||
const date = moment(session.startTime).format('YYYY-MM-DD');
|
||||
const fileName = `session-${date}-${session.id.substring(0, 8)}.md`;
|
||||
const filePath = join(notesDir, fileName);
|
||||
|
||||
writeFileSync(filePath, summary);
|
||||
|
||||
// Update CLAUDE.md with reference to latest session
|
||||
await this.updateClaudeMdWithSessionReference(project, fileName);
|
||||
} catch (error) {
|
||||
console.warn('Failed to save session notes to file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateClaudeMdWithSessionReference(project: Project, sessionFileName: string): Promise<void> {
|
||||
try {
|
||||
const claudeMdPath = join(project.path, 'CLAUDE.md');
|
||||
if (!existsSync(claudeMdPath)) return;
|
||||
|
||||
let content = readFileSync(claudeMdPath, 'utf8');
|
||||
const sessionReference = `\n\n## Latest Session\n- [${sessionFileName}](.claude-sessions/${sessionFileName})`;
|
||||
|
||||
if (content.includes('## Recent Session Notes')) {
|
||||
content = content.replace('## Recent Session Notes', `## Recent Session Notes${sessionReference}`);
|
||||
} else {
|
||||
content += sessionReference;
|
||||
}
|
||||
|
||||
writeFileSync(claudeMdPath, content);
|
||||
} catch (error) {
|
||||
console.warn('Failed to update CLAUDE.md:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import { Database } from '../database/Database';
|
||||
import { Project, CodingStyle } from '../models';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { writeFileSync, readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export class ProjectManager {
|
||||
private db: Database;
|
||||
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async createProject(name: string, projectPath: string): Promise<Project> {
|
||||
const existingProject = await this.getProjectByPath(projectPath);
|
||||
if (existingProject) {
|
||||
throw new Error(`Project already exists at path: ${projectPath}`);
|
||||
}
|
||||
|
||||
const detectedTech = await this.detectTechStack(projectPath);
|
||||
const codingStyle = await this.analyzeCodingStyle();
|
||||
|
||||
const project: Project = {
|
||||
id: uuidv4(),
|
||||
name,
|
||||
path: projectPath,
|
||||
techStack: detectedTech,
|
||||
codingStyle,
|
||||
commonCommands: await this.detectCommonCommands(projectPath),
|
||||
architecture: await this.analyzeArchitecture(projectPath),
|
||||
createdAt: new Date(),
|
||||
lastActive: new Date(),
|
||||
totalSessions: 0,
|
||||
totalTokensUsed: 0
|
||||
};
|
||||
|
||||
await this.db.run(
|
||||
`INSERT INTO projects (id, name, path, techStack, codingStyle, claudeMdContent, commonCommands, architecture, createdAt, lastActive, totalSessions, totalTokensUsed)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
project.id,
|
||||
project.name,
|
||||
project.path,
|
||||
JSON.stringify(project.techStack),
|
||||
JSON.stringify(project.codingStyle),
|
||||
project.claudeMdContent,
|
||||
JSON.stringify(project.commonCommands),
|
||||
project.architecture,
|
||||
project.createdAt.toISOString(),
|
||||
project.lastActive.toISOString(),
|
||||
project.totalSessions,
|
||||
project.totalTokensUsed
|
||||
]
|
||||
);
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
async getProject(projectId: string): Promise<Project | null> {
|
||||
const row = await this.db.get('SELECT * FROM projects WHERE id = ?', [projectId]);
|
||||
return row ? this.mapRowToProject(row as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
async getProjectByPath(projectPath: string): Promise<Project | null> {
|
||||
const row = await this.db.get('SELECT * FROM projects WHERE path = ?', [projectPath]);
|
||||
return row ? this.mapRowToProject(row as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
async getAllProjects(): Promise<Project[]> {
|
||||
const rows = await this.db.all('SELECT * FROM projects ORDER BY lastActive DESC');
|
||||
return rows.map(row => this.mapRowToProject(row as Record<string, unknown>));
|
||||
}
|
||||
|
||||
async updateProject(projectId: string, updates: Partial<Project>): Promise<void> {
|
||||
const setClause = [];
|
||||
const values = [];
|
||||
|
||||
if (updates.name) {
|
||||
setClause.push('name = ?');
|
||||
values.push(updates.name);
|
||||
}
|
||||
if (updates.techStack) {
|
||||
setClause.push('techStack = ?');
|
||||
values.push(JSON.stringify(updates.techStack));
|
||||
}
|
||||
if (updates.codingStyle) {
|
||||
setClause.push('codingStyle = ?');
|
||||
values.push(JSON.stringify(updates.codingStyle));
|
||||
}
|
||||
if (updates.claudeMdContent) {
|
||||
setClause.push('claudeMdContent = ?');
|
||||
values.push(updates.claudeMdContent);
|
||||
}
|
||||
if (updates.commonCommands) {
|
||||
setClause.push('commonCommands = ?');
|
||||
values.push(JSON.stringify(updates.commonCommands));
|
||||
}
|
||||
if (updates.architecture) {
|
||||
setClause.push('architecture = ?');
|
||||
values.push(updates.architecture);
|
||||
}
|
||||
|
||||
setClause.push('lastActive = ?');
|
||||
values.push(new Date().toISOString());
|
||||
values.push(projectId);
|
||||
|
||||
if (setClause.length > 0) {
|
||||
await this.db.run(
|
||||
`UPDATE projects SET ${setClause.join(', ')} WHERE id = ?`,
|
||||
values
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async generateClaudeMd(projectId: string): Promise<string> {
|
||||
const project = await this.getProject(projectId);
|
||||
if (!project) {
|
||||
throw new Error('Project not found');
|
||||
}
|
||||
|
||||
const template = `# ${project.name}
|
||||
|
||||
## Project Overview
|
||||
${project.architecture}
|
||||
|
||||
## Tech Stack
|
||||
${project.techStack.map(tech => `- **${tech}**`).join('\n')}
|
||||
|
||||
## Coding Style
|
||||
- **Naming Convention**: ${project.codingStyle.namingConvention}
|
||||
- **File Structure**: ${project.codingStyle.fileStructure}
|
||||
|
||||
### Conventions
|
||||
${project.codingStyle.conventions.map(conv => `- ${conv}`).join('\n')}
|
||||
|
||||
## Common Commands
|
||||
${project.commonCommands.map(cmd => `- \`${cmd}\``).join('\n')}
|
||||
|
||||
## Development Guidelines
|
||||
- Use \`/clear\` frequently to reset conversation history
|
||||
- Batch similar requests into single prompts
|
||||
- Use Sonnet for implementation, Opus for complex architecture tasks
|
||||
- Always run tests and linting before committing changes
|
||||
|
||||
## Project Structure
|
||||
\`\`\`
|
||||
${await this.generateFileStructure(project.path)}
|
||||
\`\`\`
|
||||
|
||||
## Recent Session Notes
|
||||
*Session notes will be automatically added here*
|
||||
|
||||
---
|
||||
*This file is automatically generated by Claude Session Manager*
|
||||
`;
|
||||
|
||||
// Update the project with the generated content
|
||||
await this.updateProject(projectId, { claudeMdContent: template });
|
||||
|
||||
// Write to file system
|
||||
const claudeMdPath = join(project.path, 'CLAUDE.md');
|
||||
writeFileSync(claudeMdPath, template);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
async detectTechStack(projectPath: string): Promise<string[]> {
|
||||
const techStack: string[] = [];
|
||||
|
||||
try {
|
||||
// Check package.json
|
||||
const packageJsonPath = join(projectPath, 'package.json');
|
||||
if (existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
techStack.push('Node.js');
|
||||
|
||||
if (packageJson.dependencies?.react) techStack.push('React');
|
||||
if (packageJson.dependencies?.vue) techStack.push('Vue');
|
||||
if (packageJson.dependencies?.angular) techStack.push('Angular');
|
||||
if (packageJson.dependencies?.express) techStack.push('Express');
|
||||
if (packageJson.dependencies?.typescript) techStack.push('TypeScript');
|
||||
}
|
||||
|
||||
// Check for Python files
|
||||
const pythonFiles = this.findFiles(projectPath, '.py');
|
||||
if (pythonFiles.length > 0) {
|
||||
techStack.push('Python');
|
||||
|
||||
// Check for common Python frameworks
|
||||
const requirementsPath = join(projectPath, 'requirements.txt');
|
||||
if (existsSync(requirementsPath)) {
|
||||
const requirements = readFileSync(requirementsPath, 'utf8');
|
||||
if (requirements.includes('django')) techStack.push('Django');
|
||||
if (requirements.includes('flask')) techStack.push('Flask');
|
||||
if (requirements.includes('fastapi')) techStack.push('FastAPI');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for other common files
|
||||
if (this.findFiles(projectPath, '.go').length > 0) techStack.push('Go');
|
||||
if (this.findFiles(projectPath, '.rs').length > 0) techStack.push('Rust');
|
||||
if (this.findFiles(projectPath, '.java').length > 0) techStack.push('Java');
|
||||
if (this.findFiles(projectPath, '.cpp').length > 0 || this.findFiles(projectPath, '.c').length > 0) techStack.push('C/C++');
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Error detecting tech stack:', error);
|
||||
}
|
||||
|
||||
return techStack.length > 0 ? techStack : ['Unknown'];
|
||||
}
|
||||
|
||||
private async analyzeCodingStyle(): Promise<CodingStyle> {
|
||||
// Default coding style - in a real implementation, this would analyze the codebase
|
||||
return {
|
||||
namingConvention: 'camelCase',
|
||||
fileStructure: 'Organized by feature/module',
|
||||
conventions: [
|
||||
'Use meaningful variable names',
|
||||
'Write self-documenting code',
|
||||
'Follow DRY principles',
|
||||
'Include JSDoc comments for complex functions'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private async detectCommonCommands(projectPath: string): Promise<string[]> {
|
||||
const commands: string[] = [];
|
||||
|
||||
try {
|
||||
const packageJsonPath = join(projectPath, 'package.json');
|
||||
if (existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
if (packageJson.scripts) {
|
||||
Object.keys(packageJson.scripts).forEach(script => {
|
||||
commands.push(`npm run ${script}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error detecting common commands:', error);
|
||||
}
|
||||
|
||||
// Add common commands based on detected tech stack
|
||||
const techStack = await this.detectTechStack(projectPath);
|
||||
if (techStack.includes('TypeScript')) {
|
||||
commands.push('npm run typecheck', 'npm run build');
|
||||
}
|
||||
if (techStack.includes('React')) {
|
||||
commands.push('npm run dev', 'npm run test');
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
private async analyzeArchitecture(projectPath: string): Promise<string> {
|
||||
// Basic architecture analysis - in a real implementation, this would be more sophisticated
|
||||
const techStack = await this.detectTechStack(projectPath);
|
||||
return `This is a ${techStack.join('/')} project. The architecture follows modern best practices with modular design and clear separation of concerns.`;
|
||||
}
|
||||
|
||||
private async generateFileStructure(projectPath: string): Promise<string> {
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
return execSync(`find ${projectPath} -type f -name ".*" -prune -o -type f -print | head -20`, { encoding: 'utf8' })
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map(file => file.replace(projectPath + '/', ''))
|
||||
.join('\n');
|
||||
} catch {
|
||||
return 'File structure not available';
|
||||
}
|
||||
} catch {
|
||||
return 'File structure not available';
|
||||
}
|
||||
}
|
||||
|
||||
private findFiles(dir: string, extension: string): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
try {
|
||||
const items = readdirSync(dir);
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = join(dir, item);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
|
||||
files.push(...this.findFiles(fullPath, extension));
|
||||
} else if (item.endsWith(extension)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore permission errors
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private mapRowToProject(row: Record<string, unknown>): Project {
|
||||
return {
|
||||
id: row.id as string,
|
||||
name: row.name as string,
|
||||
path: row.path as string,
|
||||
techStack: JSON.parse((row.techStack as string) || '[]'),
|
||||
codingStyle: JSON.parse((row.codingStyle as string) || '{}'),
|
||||
claudeMdContent: row.claudeMdContent as string,
|
||||
commonCommands: JSON.parse((row.commonCommands as string) || '[]'),
|
||||
architecture: row.architecture as string,
|
||||
createdAt: new Date(row.createdAt as string),
|
||||
lastActive: new Date(row.lastActive as string),
|
||||
totalSessions: row.totalSessions as number,
|
||||
totalTokensUsed: row.totalTokensUsed as number
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import { Database } from '../database/Database';
|
||||
import { Session, WindowSchedule } from '../models';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import moment from 'moment';
|
||||
|
||||
export class SessionManager {
|
||||
private db: Database;
|
||||
private readonly WINDOW_DURATION = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
|
||||
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async createSession(projectId: string, modelType: 'opus' | 'sonnet' = 'sonnet'): Promise<Session> {
|
||||
const session: Session = {
|
||||
id: uuidv4(),
|
||||
projectId,
|
||||
startTime: new Date(),
|
||||
modelType,
|
||||
tokensUsed: 0,
|
||||
messages: 0,
|
||||
notes: '',
|
||||
summary: '',
|
||||
status: 'active',
|
||||
optimizationScore: 0
|
||||
};
|
||||
|
||||
await this.db.run(
|
||||
`INSERT INTO sessions (id, projectId, startTime, modelType, tokensUsed, messages, notes, summary, status, optimizationScore)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[session.id, session.projectId, session.startTime.toISOString(), session.modelType,
|
||||
session.tokensUsed, session.messages, session.notes, session.summary, session.status, session.optimizationScore]
|
||||
);
|
||||
|
||||
// Create or update window schedule
|
||||
await this.updateWindowSchedule();
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
async endSession(sessionId: string, summary: string = ''): Promise<Session> {
|
||||
const endTime = new Date();
|
||||
|
||||
await this.db.run(
|
||||
`UPDATE sessions SET endTime = ?, status = 'completed', summary = ? WHERE id = ?`,
|
||||
[endTime.toISOString(), summary, sessionId]
|
||||
);
|
||||
|
||||
const session = await this.getSession(sessionId);
|
||||
if (session) {
|
||||
await this.updateProjectStats(session.projectId);
|
||||
await this.updateUsageStats(session);
|
||||
}
|
||||
|
||||
return session!;
|
||||
}
|
||||
|
||||
async getSession(sessionId: string): Promise<Session | null> {
|
||||
const row = await this.db.get('SELECT * FROM sessions WHERE id = ?', [sessionId]);
|
||||
return row ? this.mapRowToSession(row as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
async getActiveSession(): Promise<Session | null> {
|
||||
const row = await this.db.get('SELECT * FROM sessions WHERE status = "active" ORDER BY startTime DESC LIMIT 1');
|
||||
return row ? this.mapRowToSession(row as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
async updateSessionTokens(sessionId: string, tokensUsed: number, messages: number): Promise<void> {
|
||||
await this.db.run(
|
||||
'UPDATE sessions SET tokensUsed = ?, messages = ? WHERE id = ?',
|
||||
[tokensUsed, messages, sessionId]
|
||||
);
|
||||
}
|
||||
|
||||
async scheduleOverlapHack(sessionId: string, delayHours: number = 2): Promise<WindowSchedule> {
|
||||
const startTime = new Date(Date.now() + delayHours * 60 * 60 * 1000);
|
||||
const endTime = new Date(startTime.getTime() + this.WINDOW_DURATION);
|
||||
|
||||
const schedule: WindowSchedule = {
|
||||
id: uuidv4(),
|
||||
startTime,
|
||||
endTime,
|
||||
isActive: false,
|
||||
overlapSession: sessionId
|
||||
};
|
||||
|
||||
await this.db.run(
|
||||
`INSERT INTO window_schedules (id, startTime, endTime, isActive, overlapSession)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[schedule.id, schedule.startTime.toISOString(), schedule.endTime.toISOString(),
|
||||
schedule.isActive, schedule.overlapSession]
|
||||
);
|
||||
|
||||
return schedule;
|
||||
}
|
||||
|
||||
async getCurrentWindow(): Promise<WindowSchedule | null> {
|
||||
const now = new Date();
|
||||
const row = await this.db.get(
|
||||
`SELECT * FROM window_schedules
|
||||
WHERE startTime <= ? AND endTime >= ? AND isActive = 1
|
||||
ORDER BY startTime DESC LIMIT 1`,
|
||||
[now.toISOString(), now.toISOString()]
|
||||
);
|
||||
|
||||
return row ? this.mapRowToWindowSchedule(row as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
async getNextAvailableWindow(): Promise<Date | null> {
|
||||
const now = new Date();
|
||||
const row = await this.db.get(
|
||||
`SELECT startTime FROM window_schedules
|
||||
WHERE startTime > ? ORDER BY startTime ASC LIMIT 1`,
|
||||
[now.toISOString()]
|
||||
);
|
||||
|
||||
return row ? new Date((row as { startTime: string }).startTime) : null;
|
||||
}
|
||||
|
||||
async getSessionHistory(limit: number = 10): Promise<Session[]> {
|
||||
const rows = await this.db.all(
|
||||
'SELECT * FROM sessions ORDER BY startTime DESC LIMIT ?',
|
||||
[limit]
|
||||
);
|
||||
return rows.map(row => this.mapRowToSession(row as Record<string, unknown>));
|
||||
}
|
||||
|
||||
private async updateWindowSchedule(): Promise<void> {
|
||||
const now = new Date();
|
||||
const currentWindow = await this.getCurrentWindow();
|
||||
|
||||
if (!currentWindow) {
|
||||
// Create new window
|
||||
const endTime = new Date(now.getTime() + this.WINDOW_DURATION);
|
||||
const schedule: WindowSchedule = {
|
||||
id: uuidv4(),
|
||||
startTime: now,
|
||||
endTime,
|
||||
isActive: true
|
||||
};
|
||||
|
||||
await this.db.run(
|
||||
`INSERT INTO window_schedules (id, startTime, endTime, isActive)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[schedule.id, schedule.startTime.toISOString(), schedule.endTime.toISOString(), schedule.isActive]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateProjectStats(projectId: string): Promise<void> {
|
||||
const stats = await this.db.get(
|
||||
`SELECT COUNT(*) as sessionCount, SUM(tokensUsed) as totalTokens
|
||||
FROM sessions WHERE projectId = ?`,
|
||||
[projectId]
|
||||
) as { sessionCount: number; totalTokens: number };
|
||||
|
||||
await this.db.run(
|
||||
`UPDATE projects SET totalSessions = ?, totalTokensUsed = ?, lastActive = ? WHERE id = ?`,
|
||||
[stats.sessionCount, stats.totalTokens || 0, new Date().toISOString(), projectId]
|
||||
);
|
||||
}
|
||||
|
||||
private async updateUsageStats(session: Session): Promise<void> {
|
||||
const today = moment().format('YYYY-MM-DD');
|
||||
const existingStats = await this.db.get(
|
||||
'SELECT * FROM usage_stats WHERE date = ?',
|
||||
[today]
|
||||
);
|
||||
|
||||
if (existingStats) {
|
||||
await this.db.run(
|
||||
`UPDATE usage_stats SET
|
||||
sessionsCount = sessionsCount + 1,
|
||||
totalTokens = totalTokens + ?,
|
||||
opusSessions = opusSessions + ?,
|
||||
sonnetSessions = sonnetSessions + ?
|
||||
WHERE date = ?`,
|
||||
[session.tokensUsed, session.modelType === 'opus' ? 1 : 0, session.modelType === 'sonnet' ? 1 : 0, today]
|
||||
);
|
||||
} else {
|
||||
const statsId = uuidv4();
|
||||
await this.db.run(
|
||||
`INSERT INTO usage_stats (id, date, sessionsCount, totalTokens, opusSessions, sonnetSessions)
|
||||
VALUES (?, ?, 1, ?, ?, ?)`,
|
||||
[statsId, today, session.tokensUsed, session.modelType === 'opus' ? 1 : 0, session.modelType === 'sonnet' ? 1 : 0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private mapRowToSession(row: Record<string, unknown>): Session {
|
||||
return {
|
||||
id: row.id as string,
|
||||
projectId: row.projectId as string,
|
||||
startTime: new Date(row.startTime as string),
|
||||
endTime: row.endTime ? new Date(row.endTime as string) : undefined,
|
||||
modelType: row.modelType as 'opus' | 'sonnet',
|
||||
tokensUsed: row.tokensUsed as number,
|
||||
messages: row.messages as number,
|
||||
notes: row.notes as string,
|
||||
summary: row.summary as string,
|
||||
status: row.status as 'active' | 'completed' | 'interrupted',
|
||||
optimizationScore: row.optimizationScore as number
|
||||
};
|
||||
}
|
||||
|
||||
private mapRowToWindowSchedule(row: Record<string, unknown>): WindowSchedule {
|
||||
return {
|
||||
id: row.id as string,
|
||||
startTime: new Date(row.startTime as string),
|
||||
endTime: new Date(row.endTime as string),
|
||||
isActive: Boolean(row.isActive),
|
||||
overlapSession: row.overlapSession as string
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { Database } from '../database/Database';
|
||||
import { UsageStats, OptimizationRecommendation, Session } from '../models';
|
||||
import moment from 'moment';
|
||||
|
||||
export class TokenTracker {
|
||||
private db: Database;
|
||||
private readonly DAILY_TOKEN_LIMIT = {
|
||||
opus: 100000, // Estimated daily limit for Opus
|
||||
sonnet: 500000 // Estimated daily limit for Sonnet
|
||||
};
|
||||
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async trackTokenUsage(sessionId: string, tokensUsed: number): Promise<void> {
|
||||
await this.db.run(
|
||||
'UPDATE sessions SET tokensUsed = ? WHERE id = ?',
|
||||
[tokensUsed, sessionId]
|
||||
);
|
||||
|
||||
// Update daily usage stats
|
||||
await this.updateDailyUsage(tokensUsed);
|
||||
}
|
||||
|
||||
async getUsageStats(days: number = 7): Promise<UsageStats[]> {
|
||||
const startDate = moment().subtract(days, 'days').format('YYYY-MM-DD');
|
||||
|
||||
const rows = await this.db.all(
|
||||
`SELECT * FROM usage_stats
|
||||
WHERE date >= ?
|
||||
ORDER BY date DESC`,
|
||||
[startDate]
|
||||
);
|
||||
|
||||
return rows.map(row => this.mapRowToUsageStats(row as Record<string, unknown>));
|
||||
}
|
||||
|
||||
async getTodayUsage(): Promise<UsageStats | null> {
|
||||
const today = moment().format('YYYY-MM-DD');
|
||||
const row = await this.db.get(
|
||||
'SELECT * FROM usage_stats WHERE date = ?',
|
||||
[today]
|
||||
);
|
||||
|
||||
return row ? this.mapRowToUsageStats(row as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
async getOptimizationRecommendations(): Promise<OptimizationRecommendation[]> {
|
||||
const recommendations: OptimizationRecommendation[] = [];
|
||||
const todayUsage = await this.getTodayUsage();
|
||||
const weeklyUsage = await this.getUsageStats(7);
|
||||
|
||||
if (!todayUsage) {
|
||||
return [{
|
||||
type: 'token',
|
||||
priority: 'medium',
|
||||
message: 'Start tracking your token usage to get optimization recommendations'
|
||||
}];
|
||||
}
|
||||
|
||||
// Check for high Opus usage
|
||||
if (todayUsage.opusSessions > 3) {
|
||||
recommendations.push({
|
||||
type: 'model',
|
||||
priority: 'high',
|
||||
message: `You've used Opus ${todayUsage.opusSessions} times today. Consider using Sonnet for routine tasks to save tokens.`,
|
||||
potentialSavings: todayUsage.opusSessions * 50000 // Estimated savings per session
|
||||
});
|
||||
}
|
||||
|
||||
// Check for low optimization score
|
||||
if (todayUsage.optimizationScore < 70) {
|
||||
recommendations.push({
|
||||
type: 'workflow',
|
||||
priority: 'medium',
|
||||
message: 'Your optimization score is low. Try batching requests and using /compact more frequently.'
|
||||
});
|
||||
}
|
||||
|
||||
// Check daily token limits
|
||||
const totalWeeklyTokens = weeklyUsage.reduce((sum, day) => sum + day.totalTokens, 0);
|
||||
const dailyAverage = totalWeeklyTokens / 7;
|
||||
|
||||
if (dailyAverage > this.DAILY_TOKEN_LIMIT.sonnet * 0.8) {
|
||||
recommendations.push({
|
||||
type: 'token',
|
||||
priority: 'high',
|
||||
message: 'You\'re approaching your daily token limits. Consider using the overlap hack to schedule sessions.',
|
||||
potentialSavings: this.DAILY_TOKEN_LIMIT.sonnet - dailyAverage
|
||||
});
|
||||
}
|
||||
|
||||
// Check session patterns
|
||||
const shortSessions = weeklyUsage.filter(day => day.averageSessionLength < 10).length;
|
||||
if (shortSessions > 3) {
|
||||
recommendations.push({
|
||||
type: 'scheduling',
|
||||
priority: 'medium',
|
||||
message: 'You have many short sessions. Try batching similar tasks into longer, more focused sessions.'
|
||||
});
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
async calculateOptimizationScore(session: Session): Promise<number> {
|
||||
let score = 100;
|
||||
|
||||
// Deduct points for high token usage
|
||||
if (session.tokensUsed > 50000) {
|
||||
score -= 20;
|
||||
} else if (session.tokensUsed > 25000) {
|
||||
score -= 10;
|
||||
}
|
||||
|
||||
// Deduct points for short sessions (less efficient)
|
||||
const sessionLength = session.endTime
|
||||
? (session.endTime.getTime() - session.startTime.getTime()) / (1000 * 60) // minutes
|
||||
: 0;
|
||||
|
||||
if (sessionLength < 10) {
|
||||
score -= 15;
|
||||
} else if (sessionLength < 20) {
|
||||
score -= 5;
|
||||
}
|
||||
|
||||
// Bonus points for using Sonnet (more efficient)
|
||||
if (session.modelType === 'sonnet') {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
// Deduct points for low message count (might indicate inefficient communication)
|
||||
if (session.messages < 5) {
|
||||
score -= 10;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(100, score));
|
||||
}
|
||||
|
||||
async getTokenEfficiencyTrend(days: number = 30): Promise<{date: string, efficiency: number}[]> {
|
||||
const startDate = moment().subtract(days, 'days').format('YYYY-MM-DD');
|
||||
|
||||
const rows = await this.db.all(
|
||||
`SELECT date,
|
||||
(totalTokens / NULLIF(sessionsCount, 0)) as avgTokensPerSession,
|
||||
optimizationScore
|
||||
FROM usage_stats
|
||||
WHERE date >= ?
|
||||
ORDER BY date ASC`,
|
||||
[startDate]
|
||||
);
|
||||
|
||||
return rows.map(row => ({
|
||||
date: (row as { date: string }).date,
|
||||
efficiency: (row as { optimizationScore: number }).optimizationScore || 0
|
||||
}));
|
||||
}
|
||||
|
||||
async projectTokenBreakdown(projectId: string): Promise<{total: number, byModel: {opus: number, sonnet: number}}> {
|
||||
const row = await this.db.get(
|
||||
`SELECT
|
||||
SUM(tokensUsed) as total,
|
||||
SUM(CASE WHEN modelType = 'opus' THEN tokensUsed ELSE 0 END) as opus,
|
||||
SUM(CASE WHEN modelType = 'sonnet' THEN tokensUsed ELSE 0 END) as sonnet
|
||||
FROM sessions
|
||||
WHERE projectId = ?`,
|
||||
[projectId]
|
||||
);
|
||||
|
||||
return {
|
||||
total: (row as { total: number }).total || 0,
|
||||
byModel: {
|
||||
opus: (row as { opus: number }).opus || 0,
|
||||
sonnet: (row as { sonnet: number }).sonnet || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async updateDailyUsage(tokensUsed: number): Promise<void> {
|
||||
const today = moment().format('YYYY-MM-DD');
|
||||
|
||||
await this.db.run(
|
||||
`UPDATE usage_stats SET totalTokens = totalTokens + ? WHERE date = ?`,
|
||||
[tokensUsed, today]
|
||||
);
|
||||
}
|
||||
|
||||
private mapRowToUsageStats(row: Record<string, unknown>): UsageStats {
|
||||
return {
|
||||
id: row.id as string,
|
||||
date: new Date(row.date as string),
|
||||
sessionsCount: row.sessionsCount as number,
|
||||
totalTokens: row.totalTokens as number,
|
||||
opusSessions: row.opusSessions as number,
|
||||
sonnetSessions: row.sonnetSessions as number,
|
||||
averageSessionLength: row.averageSessionLength as number,
|
||||
optimizationScore: row.optimizationScore as number,
|
||||
recommendations: row.recommendations ? JSON.parse(row.recommendations as string) : []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
import { Database } from '../database/Database';
|
||||
import { OptimizationRecommendation } from '../models';
|
||||
import { TokenTracker } from './TokenTracker';
|
||||
import { SessionManager } from './SessionManager';
|
||||
|
||||
export class WorkflowOptimizer {
|
||||
private db: Database;
|
||||
private tokenTracker: TokenTracker;
|
||||
private sessionManager: SessionManager;
|
||||
|
||||
constructor(db: Database, tokenTracker: TokenTracker, sessionManager: SessionManager) {
|
||||
this.db = db;
|
||||
this.tokenTracker = tokenTracker;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
async analyzeWorkflowEfficiency(projectId?: string): Promise<{
|
||||
score: number;
|
||||
recommendations: OptimizationRecommendation[];
|
||||
insights: string[];
|
||||
}> {
|
||||
const recommendations: OptimizationRecommendation[] = [];
|
||||
const insights: string[] = [];
|
||||
let score = 100;
|
||||
|
||||
// Analyze session patterns
|
||||
const sessionAnalysis = await this.analyzeSessionPatterns(projectId);
|
||||
score -= sessionAnalysis.penalties;
|
||||
recommendations.push(...sessionAnalysis.recommendations);
|
||||
insights.push(...sessionAnalysis.insights);
|
||||
|
||||
// Analyze token usage
|
||||
const tokenAnalysis = await this.analyzeTokenUsage();
|
||||
score -= tokenAnalysis.penalties;
|
||||
recommendations.push(...tokenAnalysis.recommendations);
|
||||
insights.push(...tokenAnalysis.insights);
|
||||
|
||||
// Analyze scheduling efficiency
|
||||
const schedulingAnalysis = await this.analyzeSchedulingEfficiency();
|
||||
score -= schedulingAnalysis.penalties;
|
||||
recommendations.push(...schedulingAnalysis.recommendations);
|
||||
insights.push(...schedulingAnalysis.insights);
|
||||
|
||||
return {
|
||||
score: Math.max(0, Math.min(100, score)),
|
||||
recommendations: this.prioritizeRecommendations(recommendations),
|
||||
insights
|
||||
};
|
||||
}
|
||||
|
||||
async suggestOptimalSession(taskDescription: string, complexity: 'simple' | 'medium' | 'complex'): Promise<{
|
||||
modelType: 'opus' | 'sonnet';
|
||||
estimatedTokens: number;
|
||||
batchingSuggestions: string[];
|
||||
preparationTips: string[];
|
||||
}> {
|
||||
const modelType = this.recommendModel(taskDescription, complexity);
|
||||
const estimatedTokens = this.estimateTokenUsage(taskDescription, complexity, modelType);
|
||||
|
||||
return {
|
||||
modelType,
|
||||
estimatedTokens,
|
||||
batchingSuggestions: this.generateBatchingSuggestions(taskDescription),
|
||||
preparationTips: this.generatePreparationTips(complexity)
|
||||
};
|
||||
}
|
||||
|
||||
async generateOptimalSchedule(upcomingTasks: Array<{
|
||||
description: string;
|
||||
complexity: 'simple' | 'medium' | 'complex';
|
||||
estimatedDuration: number; // minutes
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}>): Promise<{
|
||||
schedule: Array<{
|
||||
task: string;
|
||||
recommendedTime: Date;
|
||||
modelType: 'opus' | 'sonnet';
|
||||
grouping: string;
|
||||
}>;
|
||||
totalEstimatedTokens: number;
|
||||
optimizationNotes: string[];
|
||||
}> {
|
||||
const schedule = [];
|
||||
let totalTokens = 0;
|
||||
const optimizationNotes = [];
|
||||
|
||||
// Group tasks by complexity and type
|
||||
const groupedTasks = this.groupTasksByType(upcomingTasks);
|
||||
|
||||
// Get current window status
|
||||
const currentWindow = await this.sessionManager.getCurrentWindow();
|
||||
const nextWindow = await this.sessionManager.getNextAvailableWindow();
|
||||
|
||||
let currentTime = currentWindow ? new Date() : (nextWindow || new Date());
|
||||
|
||||
for (const [groupName, tasks] of Object.entries(groupedTasks)) {
|
||||
for (const task of tasks) {
|
||||
const suggestion = await this.suggestOptimalSession(task.description, task.complexity);
|
||||
|
||||
schedule.push({
|
||||
task: task.description,
|
||||
recommendedTime: new Date(currentTime),
|
||||
modelType: suggestion.modelType,
|
||||
grouping: groupName
|
||||
});
|
||||
|
||||
totalTokens += suggestion.estimatedTokens;
|
||||
currentTime = new Date(currentTime.getTime() + task.estimatedDuration * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Add optimization notes
|
||||
if (totalTokens > 400000) {
|
||||
optimizationNotes.push('High token usage expected. Consider splitting across multiple 5-hour windows');
|
||||
}
|
||||
|
||||
const opusTasks = upcomingTasks.filter(t => t.complexity === 'complex').length;
|
||||
if (opusTasks > 3) {
|
||||
optimizationNotes.push('Multiple complex tasks detected. Consider using overlap hack for extended sessions');
|
||||
}
|
||||
|
||||
return {
|
||||
schedule,
|
||||
totalEstimatedTokens: totalTokens,
|
||||
optimizationNotes
|
||||
};
|
||||
}
|
||||
|
||||
async detectWorkflowAnomalies(): Promise<{
|
||||
anomalies: Array<{
|
||||
type: 'token_spike' | 'session_fragmentation' | 'model_misuse' | 'scheduling_conflict';
|
||||
severity: 'low' | 'medium' | 'high';
|
||||
description: string;
|
||||
recommendation: string;
|
||||
}>;
|
||||
trends: Array<{
|
||||
metric: string;
|
||||
trend: 'improving' | 'declining' | 'stable';
|
||||
change: number;
|
||||
}>;
|
||||
}> {
|
||||
const anomalies = [];
|
||||
const trends = [];
|
||||
|
||||
// Detect token spikes
|
||||
const recentUsage = await this.tokenTracker.getUsageStats(7);
|
||||
const avgDailyTokens = recentUsage.reduce((sum, day) => sum + day.totalTokens, 0) / 7;
|
||||
|
||||
if (avgDailyTokens > 100000) {
|
||||
anomalies.push({
|
||||
type: 'token_spike',
|
||||
severity: 'high',
|
||||
description: `Daily token usage (${avgDailyTokens.toLocaleString()}) is unusually high`,
|
||||
recommendation: 'Review recent sessions for inefficiencies and consider using /compact more frequently'
|
||||
});
|
||||
}
|
||||
|
||||
// Detect session fragmentation
|
||||
const recentSessions = await this.sessionManager.getSessionHistory(20);
|
||||
const shortSessions = recentSessions.filter(s => {
|
||||
const duration = s.endTime ? (s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60) : 0;
|
||||
return duration < 10;
|
||||
}).length;
|
||||
|
||||
if (shortSessions > recentSessions.length * 0.5) {
|
||||
anomalies.push({
|
||||
type: 'session_fragmentation',
|
||||
severity: 'medium',
|
||||
description: `${shortSessions} out of ${recentSessions.length} recent sessions were shorter than 10 minutes`,
|
||||
recommendation: 'Consider batching similar tasks into longer, more focused sessions'
|
||||
});
|
||||
}
|
||||
|
||||
// Detect model misuse
|
||||
const opusHeavyDays = recentUsage.filter(day => day.opusSessions > day.sonnetSessions).length;
|
||||
if (opusHeavyDays > recentUsage.length * 0.6) {
|
||||
anomalies.push({
|
||||
type: 'model_misuse',
|
||||
severity: 'medium',
|
||||
description: 'Opus is being used more frequently than Sonnet',
|
||||
recommendation: 'Reserve Opus for complex tasks and use Sonnet for routine implementation work'
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate trends
|
||||
if (recentUsage.length >= 2) {
|
||||
const recentAvg = recentUsage.slice(0, 3).reduce((sum, day) => sum + day.optimizationScore, 0) / 3;
|
||||
const olderAvg = recentUsage.slice(3, 6).reduce((sum, day) => sum + day.optimizationScore, 0) / Math.min(3, recentUsage.length - 3);
|
||||
|
||||
if (recentAvg > olderAvg + 5) {
|
||||
trends.push({ metric: 'optimization_score', trend: 'improving', change: recentAvg - olderAvg });
|
||||
} else if (recentAvg < olderAvg - 5) {
|
||||
trends.push({ metric: 'optimization_score', trend: 'declining', change: olderAvg - recentAvg });
|
||||
} else {
|
||||
trends.push({ metric: 'optimization_score', trend: 'stable', change: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
return { anomalies, trends };
|
||||
}
|
||||
|
||||
private async analyzeSessionPatterns(projectId?: string): Promise<{
|
||||
penalties: number;
|
||||
recommendations: OptimizationRecommendation[];
|
||||
insights: string[];
|
||||
}> {
|
||||
const penalties = 0;
|
||||
const recommendations: OptimizationRecommendation[] = [];
|
||||
const insights: string[] = [];
|
||||
|
||||
const sessions = projectId
|
||||
? await this.db.all('SELECT * FROM sessions WHERE projectId = ? ORDER BY startTime DESC LIMIT 10', [projectId])
|
||||
: await this.sessionManager.getSessionHistory(10);
|
||||
|
||||
// Analyze session length distribution
|
||||
const sessionLengths = sessions.map(session => {
|
||||
const endTime = (session as { endTime?: string | Date }).endTime || new Date();
|
||||
const endTimeDate = typeof endTime === 'string' ? new Date(endTime) : endTime;
|
||||
return (endTimeDate.getTime() - new Date((session as { startTime: string }).startTime).getTime()) / (1000 * 60); // minutes
|
||||
});
|
||||
|
||||
const avgLength = sessionLengths.reduce((sum, length) => sum + length, 0) / sessionLengths.length;
|
||||
|
||||
if (avgLength < 15) {
|
||||
recommendations.push({
|
||||
type: 'workflow',
|
||||
priority: 'medium',
|
||||
message: 'Average session length is quite short. Consider batching related tasks.'
|
||||
});
|
||||
}
|
||||
|
||||
insights.push(`Average session duration: ${avgLength.toFixed(1)} minutes`);
|
||||
|
||||
return { penalties, recommendations, insights };
|
||||
}
|
||||
|
||||
private async analyzeTokenUsage(): Promise<{
|
||||
penalties: number;
|
||||
recommendations: OptimizationRecommendation[];
|
||||
insights: string[];
|
||||
}> {
|
||||
const penalties = 0;
|
||||
const recommendations: OptimizationRecommendation[] = [];
|
||||
const insights: string[] = [];
|
||||
|
||||
const usageStats = await this.tokenTracker.getUsageStats(7);
|
||||
usageStats.reduce((sum, day) => sum + day.totalTokens, 0);
|
||||
const avgTokensPerSession = usageStats.reduce((sum, day) => sum + (day.totalTokens / Math.max(1, day.sessionsCount)), 0) / usageStats.length;
|
||||
|
||||
if (avgTokensPerSession > 50000) {
|
||||
recommendations.push({
|
||||
type: 'token',
|
||||
priority: 'high',
|
||||
message: 'High token usage per session detected. Consider using /compact more frequently.',
|
||||
potentialSavings: avgTokensPerSession * 0.3
|
||||
});
|
||||
}
|
||||
|
||||
insights.push(`Average tokens per session: ${avgTokensPerSession.toLocaleString()}`);
|
||||
|
||||
return { penalties, recommendations, insights };
|
||||
}
|
||||
|
||||
private async analyzeSchedulingEfficiency(): Promise<{
|
||||
penalties: number;
|
||||
recommendations: OptimizationRecommendation[];
|
||||
insights: string[];
|
||||
}> {
|
||||
const penalties = 0;
|
||||
const recommendations: OptimizationRecommendation[] = [];
|
||||
const insights: string[] = [];
|
||||
|
||||
const currentWindow = await this.sessionManager.getCurrentWindow();
|
||||
|
||||
if (!currentWindow) {
|
||||
recommendations.push({
|
||||
type: 'scheduling',
|
||||
priority: 'medium',
|
||||
message: 'No active 5-hour window. Consider using the overlap hack to prepare for intensive sessions.'
|
||||
});
|
||||
}
|
||||
|
||||
insights.push(currentWindow ? 'Currently within an active 5-hour window' : 'No active window detected');
|
||||
|
||||
return { penalties, recommendations, insights };
|
||||
}
|
||||
|
||||
private recommendModel(taskDescription: string, complexity: 'simple' | 'medium' | 'complex'): 'opus' | 'sonnet' {
|
||||
if (complexity === 'complex') return 'opus';
|
||||
if (complexity === 'simple') return 'sonnet';
|
||||
|
||||
// For medium complexity, analyze task description
|
||||
const complexKeywords = ['architecture', 'design', 'refactor', 'optimize', 'debug complex', 'implement system'];
|
||||
const hasComplexKeywords = complexKeywords.some(keyword =>
|
||||
taskDescription.toLowerCase().includes(keyword)
|
||||
);
|
||||
|
||||
return hasComplexKeywords ? 'opus' : 'sonnet';
|
||||
}
|
||||
|
||||
private estimateTokenUsage(taskDescription: string, complexity: 'simple' | 'medium' | 'complex', modelType: 'opus' | 'sonnet'): number {
|
||||
const baseTokens = {
|
||||
simple: { sonnet: 5000, opus: 8000 },
|
||||
medium: { sonnet: 15000, opus: 25000 },
|
||||
complex: { sonnet: 30000, opus: 50000 }
|
||||
};
|
||||
|
||||
return baseTokens[complexity][modelType];
|
||||
}
|
||||
|
||||
private generateBatchingSuggestions(taskDescription: string): string[] {
|
||||
const suggestions = [
|
||||
'Prepare all related files and context before starting',
|
||||
'Group similar implementation tasks together',
|
||||
'Plan testing and linting as part of the same session'
|
||||
];
|
||||
|
||||
if (taskDescription.includes('test')) {
|
||||
suggestions.push('Include test writing in the same session as implementation');
|
||||
}
|
||||
|
||||
if (taskDescription.includes('refactor')) {
|
||||
suggestions.push('Batch related refactoring tasks to maintain context');
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
private generatePreparationTips(complexity: 'simple' | 'medium' | 'complex'): string[] {
|
||||
const tips = {
|
||||
simple: [
|
||||
'Have specific files ready to reference',
|
||||
'Prepare clear, concise instructions'
|
||||
],
|
||||
medium: [
|
||||
'Review relevant documentation beforehand',
|
||||
'Prepare test cases or expected outcomes',
|
||||
'Consider edge cases in advance'
|
||||
],
|
||||
complex: [
|
||||
'Create a detailed step-by-step plan',
|
||||
'Prepare comprehensive project context',
|
||||
'Have fallback approaches ready',
|
||||
'Schedule adequate time for implementation'
|
||||
]
|
||||
};
|
||||
|
||||
return tips[complexity];
|
||||
}
|
||||
|
||||
private groupTasksByType(tasks: Array<{
|
||||
description: string;
|
||||
complexity: 'simple' | 'medium' | 'complex';
|
||||
estimatedDuration: number;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}>): Record<string, Array<{
|
||||
description: string;
|
||||
complexity: 'simple' | 'medium' | 'complex';
|
||||
estimatedDuration: number;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}>> {
|
||||
return tasks.reduce((groups, task) => {
|
||||
const category = this.categorizeTask(task);
|
||||
if (!groups[category]) groups[category] = [];
|
||||
groups[category].push(task);
|
||||
return groups;
|
||||
}, {} as Record<string, Array<{
|
||||
description: string;
|
||||
complexity: 'simple' | 'medium' | 'complex';
|
||||
estimatedDuration: number;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}>>);
|
||||
}
|
||||
|
||||
private categorizeTask(task: {
|
||||
description: string;
|
||||
complexity: 'simple' | 'medium' | 'complex';
|
||||
estimatedDuration: number;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}): string {
|
||||
if (task.complexity === 'complex') return 'Complex Architecture';
|
||||
if (task.description.includes('test')) return 'Testing';
|
||||
if (task.description.includes('fix') || task.description.includes('bug')) return 'Bug Fixes';
|
||||
if (task.description.includes('feature')) return 'Feature Development';
|
||||
return 'General Tasks';
|
||||
}
|
||||
|
||||
private prioritizeRecommendations(recommendations: OptimizationRecommendation[]): OptimizationRecommendation[] {
|
||||
return recommendations.sort((a, b) => {
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# CLAUDE.md Template
|
||||
|
||||
This file provides Claude with essential context about your project to maximize session efficiency and reduce token usage.
|
||||
|
||||
## Project Overview
|
||||
{{projectOverview}}
|
||||
|
||||
## Tech Stack
|
||||
{{techStack}}
|
||||
|
||||
## Architecture
|
||||
{{architecture}}
|
||||
|
||||
## Coding Style
|
||||
- **Naming Convention**: {{namingConvention}}
|
||||
- **File Structure**: {{fileStructure}}
|
||||
|
||||
### Conventions
|
||||
{{conventions}}
|
||||
|
||||
## Common Commands
|
||||
{{commonCommands}}
|
||||
|
||||
## Development Guidelines
|
||||
- Use `/clear` frequently to reset conversation history
|
||||
- Batch similar requests into single detailed prompts
|
||||
- Use Sonnet for implementation, Opus for complex architecture tasks
|
||||
- Always run tests and linting before committing changes
|
||||
- Reference this file instead of repeating project context
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
{{fileStructure}}
|
||||
```
|
||||
|
||||
## Recent Session Notes
|
||||
{{sessionNotes}}
|
||||
|
||||
---
|
||||
*This file is automatically generated by Claude Session Manager*
|
||||
*Last updated: {{lastUpdated}}*
|
||||
@@ -0,0 +1,30 @@
|
||||
# Session Notes - {{date}}
|
||||
|
||||
## Session Details
|
||||
- **Session ID**: {{sessionId}}
|
||||
- **Project**: {{projectName}}
|
||||
- **Model**: {{modelType}}
|
||||
- **Duration**: {{duration}}
|
||||
- **Tokens Used**: {{tokensUsed}}
|
||||
- **Messages**: {{messages}}
|
||||
|
||||
## Key Activities
|
||||
{{activities}}
|
||||
|
||||
## Files Modified
|
||||
{{filesModified}}
|
||||
|
||||
## Outcomes
|
||||
{{outcomes}}
|
||||
|
||||
## Next Steps
|
||||
{{nextSteps}}
|
||||
|
||||
## Optimization Notes
|
||||
{{optimizationNotes}}
|
||||
|
||||
## Raw Notes
|
||||
{{rawNotes}}
|
||||
|
||||
---
|
||||
*Generated by Claude Session Manager*
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noImplicitAny": false,
|
||||
"strictNullChecks": false,
|
||||
"types": ["bun-types"],
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"useDefineForClassFields": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user