mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
Compare commits
65 Commits
5cb9c1c2a5
...
v1.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db27ba1eb2 | ||
|
|
3c833d8922 | ||
|
|
156b89ed30 | ||
|
|
41ce1a52e5 | ||
|
|
6f94c2e28f | ||
|
|
91b7ccf56f | ||
|
|
7daa830da9 | ||
|
|
7e57d1b831 | ||
|
|
ff47dace11 | ||
|
|
c9dc53e862 | ||
|
|
c8f54481b8 | ||
|
|
294fc4aad8 | ||
|
|
81aa8a72c3 | ||
|
|
0e9f613fd1 | ||
|
|
1bd68ff534 | ||
|
|
24047351c2 | ||
|
|
66959c1dca | ||
|
|
5a0f6e9e1e | ||
|
|
cf61ef7539 | ||
|
|
07e23e3e64 | ||
|
|
8fc49ba0e8 | ||
|
|
b90448aef6 | ||
|
|
caab908be8 | ||
|
|
7021d1f6cf | ||
|
|
3ad211b01b | ||
|
|
f61c9b0caf | ||
|
|
b682ac7d79 | ||
|
|
e1fca6e84d | ||
|
|
07530ace5f | ||
|
|
00464b6f60 | ||
|
|
0c78a7c779 | ||
|
|
fca997001e | ||
|
|
1eca3c9130 | ||
|
|
defcdc356e | ||
|
|
b548ce47c9 | ||
|
|
90e6a8c63b | ||
|
|
c68f7efcdc | ||
|
|
aa805d5240 | ||
|
|
c5ca3c698c | ||
|
|
7e928572c7 | ||
|
|
0bf47bbb41 | ||
|
|
2ad888ca82 | ||
|
|
8966282e48 | ||
|
|
3d97985559 | ||
|
|
d54124afad | ||
|
|
0b11849f1e | ||
|
|
2c26d2d67c | ||
|
|
fdda6cbcd9 | ||
|
|
08ee723e85 | ||
|
|
f11347a708 | ||
|
|
586637f94c | ||
|
|
2b6ff6b55e | ||
|
|
2be6e09501 | ||
|
|
b1d47b22ea | ||
|
|
9dd4f4409b | ||
|
|
c5de2a7bf7 | ||
|
|
af24c617bb | ||
|
|
2ca903d4c5 | ||
|
|
4d98d9f125 | ||
|
|
9db98673d0 | ||
|
|
fab2e05ae7 | ||
|
|
8d65c6d429 | ||
|
|
9b2233b5bc | ||
|
|
5a26daf392 | ||
|
|
438d082e30 |
@@ -8,7 +8,7 @@ Pre-translated configurations for [Cursor IDE](https://cursor.com), part of the
|
||||
|----------|-------|-------------|
|
||||
| Rules | 27 | Coding standards, security, testing, patterns (common + TypeScript/Python/Go) |
|
||||
| Agents | 13 | Specialized AI agents (planner, architect, code-reviewer, tdd-guide, etc.) |
|
||||
| Skills | 37 | Agent skills for backend, frontend, security, TDD, and more |
|
||||
| Skills | 43 | Agent skills for backend, frontend, security, TDD, and more |
|
||||
| Commands | 31 | Slash commands for planning, reviewing, testing, and deployment |
|
||||
| MCP Config | 1 | Pre-configured MCP servers (GitHub, Supabase, Vercel, Railway, etc.) |
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ alwaysApply: true
|
||||
- Pair programming and code generation
|
||||
- Worker agents in multi-agent systems
|
||||
|
||||
**Sonnet 4.5** (Best coding model):
|
||||
**Sonnet 4.6** (Best coding model):
|
||||
- Main development work
|
||||
- Orchestrating multi-agent workflows
|
||||
- Complex coding tasks
|
||||
|
||||
18
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
18
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
steps:
|
||||
- name: Setup Go environment
|
||||
uses: actions/setup-go@v6.2.0
|
||||
with:
|
||||
# The Go version to download (if necessary) and use. Supports semver spec and ranges. Be sure to enclose this option in single quotation marks.
|
||||
go-version: # optional
|
||||
# Path to the go.mod, go.work, .go-version, or .tool-versions file.
|
||||
go-version-file: # optional
|
||||
# Set this option to true if you want the action to always check for the latest available version that satisfies the version spec
|
||||
check-latest: # optional
|
||||
# Used to pull Go distributions from go-versions. Since there's a default, this is typically not supplied by the user. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting.
|
||||
token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }}
|
||||
# Used to specify whether caching is needed. Set to true, if you'd like to enable caching.
|
||||
cache: # optional, default is true
|
||||
# Used to specify the path to a dependency file - go.sum
|
||||
cache-dependency-path: # optional
|
||||
# Target architecture for Go to use. Examples: x86, x64. Will use system architecture by default.
|
||||
architecture: # optional
|
||||
34
.github/workflows/security-scan.yml
vendored
34
.github/workflows/security-scan.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: AgentShield Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
# Prevent duplicate runs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Minimal permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
agentshield:
|
||||
name: AgentShield Scan
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run AgentShield Security Scan
|
||||
uses: affaan-m/agentshield@v1
|
||||
with:
|
||||
path: '.'
|
||||
min-severity: 'medium'
|
||||
format: 'terminal'
|
||||
fail-on-findings: 'false'
|
||||
60
CLAUDE.md
Normal file
60
CLAUDE.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **Claude Code plugin** - a collection of production-ready agents, skills, hooks, commands, rules, and MCP configurations. The project provides battle-tested workflows for software development using Claude Code.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
node tests/run-all.js
|
||||
|
||||
# Run individual test files
|
||||
node tests/lib/utils.test.js
|
||||
node tests/lib/package-manager.test.js
|
||||
node tests/hooks/hooks.test.js
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The project is organized into several core components:
|
||||
|
||||
- **agents/** - Specialized subagents for delegation (planner, code-reviewer, tdd-guide, etc.)
|
||||
- **skills/** - Workflow definitions and domain knowledge (coding standards, patterns, testing)
|
||||
- **commands/** - Slash commands invoked by users (/tdd, /plan, /e2e, etc.)
|
||||
- **hooks/** - Trigger-based automations (session persistence, pre/post-tool hooks)
|
||||
- **rules/** - Always-follow guidelines (security, coding style, testing requirements)
|
||||
- **mcp-configs/** - MCP server configurations for external integrations
|
||||
- **scripts/** - Cross-platform Node.js utilities for hooks and setup
|
||||
- **tests/** - Test suite for scripts and utilities
|
||||
|
||||
## Key Commands
|
||||
|
||||
- `/tdd` - Test-driven development workflow
|
||||
- `/plan` - Implementation planning
|
||||
- `/e2e` - Generate and run E2E tests
|
||||
- `/code-review` - Quality review
|
||||
- `/build-fix` - Fix build errors
|
||||
- `/learn` - Extract patterns from sessions
|
||||
- `/skill-create` - Generate skills from git history
|
||||
|
||||
## Development Notes
|
||||
|
||||
- Package manager detection: npm, pnpm, yarn, bun (configurable via `CLAUDE_PACKAGE_MANAGER` env var or project config)
|
||||
- Cross-platform: Windows, macOS, Linux support via Node.js scripts
|
||||
- Agent format: Markdown with YAML frontmatter (name, description, tools, model)
|
||||
- Skill format: Markdown with clear sections for when to use, how it works, examples
|
||||
- Hook format: JSON with matcher conditions and command/notification hooks
|
||||
|
||||
## Contributing
|
||||
|
||||
Follow the formats in CONTRIBUTING.md:
|
||||
- Agents: Markdown with frontmatter (name, description, tools, model)
|
||||
- Skills: Clear sections (When to Use, How It Works, Examples)
|
||||
- Commands: Markdown with description frontmatter
|
||||
- Hooks: JSON with matcher and hooks array
|
||||
|
||||
File naming: lowercase with hyphens (e.g., `python-reviewer.md`, `tdd-workflow.md`)
|
||||
21
README.md
21
README.md
@@ -13,7 +13,7 @@
|
||||

|
||||

|
||||
|
||||
> **42K+ stars** | **5K+ forks** | **24 contributors** | **6 languages supported** | **Anthropic Hackathon Winner**
|
||||
> **50K+ stars** | **6K+ forks** | **30 contributors** | **6 languages supported** | **Anthropic Hackathon Winner**
|
||||
|
||||
---
|
||||
|
||||
@@ -143,7 +143,7 @@ For manual install instructions see the README in the `rules/` folder.
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
✨ **That's it!** You now have access to 13 agents, 37 skills, and 31 commands.
|
||||
✨ **That's it!** You now have access to 13 agents, 44 skills, and 32 commands.
|
||||
|
||||
---
|
||||
|
||||
@@ -246,6 +246,12 @@ everything-claude-code/
|
||||
| |-- deployment-patterns/ # CI/CD, Docker, health checks, rollbacks (NEW)
|
||||
| |-- docker-patterns/ # Docker Compose, networking, volumes, container security (NEW)
|
||||
| |-- e2e-testing/ # Playwright E2E patterns and Page Object Model (NEW)
|
||||
| |-- content-hash-cache-pattern/ # SHA-256 content hash caching for file processing (NEW)
|
||||
| |-- cost-aware-llm-pipeline/ # LLM cost optimization, model routing, budget tracking (NEW)
|
||||
| |-- regex-vs-llm-structured-text/ # Decision framework: regex vs LLM for text parsing (NEW)
|
||||
| |-- swift-actor-persistence/ # Thread-safe Swift data persistence with actors (NEW)
|
||||
| |-- swift-protocol-di-testing/ # Protocol-based DI for testable Swift code (NEW)
|
||||
| |-- search-first/ # Research-before-coding workflow (NEW)
|
||||
|
|
||||
|-- commands/ # Slash commands for quick execution
|
||||
| |-- tdd.md # /tdd - Test-driven development
|
||||
@@ -255,6 +261,7 @@ everything-claude-code/
|
||||
| |-- build-fix.md # /build-fix - Fix build errors
|
||||
| |-- refactor-clean.md # /refactor-clean - Dead code removal
|
||||
| |-- learn.md # /learn - Extract patterns mid-session (Longform Guide)
|
||||
| |-- learn-eval.md # /learn-eval - Extract, evaluate, and save patterns (NEW)
|
||||
| |-- checkpoint.md # /checkpoint - Save verification state (Longform Guide)
|
||||
| |-- verify.md # /verify - Run verification loop (Longform Guide)
|
||||
| |-- setup-pm.md # /setup-pm - Configure package manager
|
||||
@@ -693,11 +700,12 @@ Each component is fully independent.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Does this work with Cursor / OpenCode?</b></summary>
|
||||
<summary><b>Does this work with Cursor / OpenCode / Codex?</b></summary>
|
||||
|
||||
Yes. ECC is cross-platform:
|
||||
- **Cursor**: Pre-translated configs in `.cursor/`. See [Cursor IDE Support](#cursor-ide-support).
|
||||
- **OpenCode**: Full plugin support in `.opencode/`. See [OpenCode Support](#-opencode-support).
|
||||
- **Codex**: First-class support with adapter drift guards and SessionStart fallback. See PR [#257](https://github.com/affaan-m/everything-claude-code/pull/257).
|
||||
- **Claude Code**: Native — this is the primary target.
|
||||
</details>
|
||||
|
||||
@@ -802,8 +810,8 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|
||||
| Feature | Claude Code | OpenCode | Status |
|
||||
|---------|-------------|----------|--------|
|
||||
| Agents | ✅ 13 agents | ✅ 12 agents | **Claude Code leads** |
|
||||
| Commands | ✅ 31 commands | ✅ 24 commands | **Claude Code leads** |
|
||||
| Skills | ✅ 37 skills | ✅ 16 skills | **Claude Code leads** |
|
||||
| Commands | ✅ 32 commands | ✅ 24 commands | **Claude Code leads** |
|
||||
| Skills | ✅ 44 skills | ✅ 16 skills | **Claude Code leads** |
|
||||
| Hooks | ✅ 3 phases | ✅ 20+ events | **OpenCode has more!** |
|
||||
| Rules | ✅ 8 rules | ✅ 8 rules | **Full parity** |
|
||||
| MCP Servers | ✅ Full | ✅ Full | **Full parity** |
|
||||
@@ -823,7 +831,7 @@ OpenCode's plugin system is MORE sophisticated than Claude Code with 20+ event t
|
||||
|
||||
**Additional OpenCode events**: `file.edited`, `file.watcher.updated`, `message.updated`, `lsp.client.diagnostics`, `tui.toast.show`, and more.
|
||||
|
||||
### Available Commands (31)
|
||||
### Available Commands (32)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
@@ -857,6 +865,7 @@ OpenCode's plugin system is MORE sophisticated than Claude Code with 20+ event t
|
||||
| `/instinct-import` | Import instincts |
|
||||
| `/instinct-export` | Export instincts |
|
||||
| `/evolve` | Cluster instincts into skills |
|
||||
| `/learn-eval` | Extract and evaluate patterns before saving |
|
||||
| `/setup-pm` | Configure package manager |
|
||||
|
||||
### Plugin Installation
|
||||
|
||||
@@ -95,7 +95,7 @@ cp -r everything-claude-code/rules/* ~/.claude/rules/
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
✨ **完成!** 你现在可以使用 13 个代理、37 个技能和 31 个命令。
|
||||
✨ **完成!** 你现在可以使用 13 个代理、43 个技能和 31 个命令。
|
||||
|
||||
---
|
||||
|
||||
|
||||
91
commands/learn-eval.md
Normal file
91
commands/learn-eval.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
description: Extract reusable patterns from the session, self-evaluate quality before saving, and determine the right save location (Global vs Project).
|
||||
---
|
||||
|
||||
# /learn-eval - Extract, Evaluate, then Save
|
||||
|
||||
Extends `/learn` with a quality gate and save-location decision before writing any skill file.
|
||||
|
||||
## What to Extract
|
||||
|
||||
Look for:
|
||||
|
||||
1. **Error Resolution Patterns** — root cause + fix + reusability
|
||||
2. **Debugging Techniques** — non-obvious steps, tool combinations
|
||||
3. **Workarounds** — library quirks, API limitations, version-specific fixes
|
||||
4. **Project-Specific Patterns** — conventions, architecture decisions, integration patterns
|
||||
|
||||
## Process
|
||||
|
||||
1. Review the session for extractable patterns
|
||||
2. Identify the most valuable/reusable insight
|
||||
|
||||
3. **Determine save location:**
|
||||
- Ask: "Would this pattern be useful in a different project?"
|
||||
- **Global** (`~/.claude/skills/learned/`): Generic patterns usable across 2+ projects (bash compatibility, LLM API behavior, debugging techniques, etc.)
|
||||
- **Project** (`.claude/skills/learned/` in current project): Project-specific knowledge (quirks of a particular config file, project-specific architecture decisions, etc.)
|
||||
- When in doubt, choose Global (moving Global → Project is easier than the reverse)
|
||||
|
||||
4. Draft the skill file using this format:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: pattern-name
|
||||
description: "Under 130 characters"
|
||||
user-invocable: false
|
||||
origin: auto-extracted
|
||||
---
|
||||
|
||||
# [Descriptive Pattern Name]
|
||||
|
||||
**Extracted:** [Date]
|
||||
**Context:** [Brief description of when this applies]
|
||||
|
||||
## Problem
|
||||
[What problem this solves - be specific]
|
||||
|
||||
## Solution
|
||||
[The pattern/technique/workaround - with code examples]
|
||||
|
||||
## When to Use
|
||||
[Trigger conditions]
|
||||
```
|
||||
|
||||
5. **Self-evaluate before saving** using this rubric:
|
||||
|
||||
| Dimension | 1 | 3 | 5 |
|
||||
|-----------|---|---|---|
|
||||
| Specificity | Abstract principles only, no code examples | Representative code example present | Rich examples covering all usage patterns |
|
||||
| Actionability | Unclear what to do | Main steps are understandable | Immediately actionable, edge cases covered |
|
||||
| Scope Fit | Too broad or too narrow | Mostly appropriate, some boundary ambiguity | Name, trigger, and content perfectly aligned |
|
||||
| Non-redundancy | Nearly identical to another skill | Some overlap but unique perspective exists | Completely unique value |
|
||||
| Coverage | Covers only a fraction of the target task | Main cases covered, common variants missing | Main cases, edge cases, and pitfalls covered |
|
||||
|
||||
- Score each dimension 1–5
|
||||
- If any dimension scores 1–2, improve the draft and re-score until all dimensions are ≥ 3
|
||||
- Show the user the scores table and the final draft
|
||||
|
||||
6. Ask user to confirm:
|
||||
- Show: proposed save path + scores table + final draft
|
||||
- Wait for explicit confirmation before writing
|
||||
|
||||
7. Save to the determined location
|
||||
|
||||
## Output Format for Step 5 (scores table)
|
||||
|
||||
| Dimension | Score | Rationale |
|
||||
|-----------|-------|-----------|
|
||||
| Specificity | N/5 | ... |
|
||||
| Actionability | N/5 | ... |
|
||||
| Scope Fit | N/5 | ... |
|
||||
| Non-redundancy | N/5 | ... |
|
||||
| Coverage | N/5 | ... |
|
||||
| **Total** | **N/25** | |
|
||||
|
||||
## Notes
|
||||
|
||||
- Don't extract trivial fixes (typos, simple syntax errors)
|
||||
- Don't extract one-time issues (specific API outages, etc.)
|
||||
- Focus on patterns that will save time in future sessions
|
||||
- Keep skills focused — one pattern per skill
|
||||
- If Coverage score is low, add related variants before saving
|
||||
@@ -140,7 +140,7 @@ cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
✨ **完了です!** これで13のエージェント、37のスキル、31のコマンドにアクセスできます。
|
||||
✨ **完了です!** これで13のエージェント、43のスキル、31のコマンドにアクセスできます。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"C": "Cyan - username, todos",
|
||||
"T": "Gray - model name"
|
||||
},
|
||||
"output_example": "affoon:~/projects/myapp main* ctx:73% sonnet-4.5 14:30 todos:3",
|
||||
"output_example": "affoon:~/projects/myapp main* ctx:73% sonnet-4.6 14:30 todos:3",
|
||||
"usage": "Copy the statusLine object to your ~/.claude/settings.json"
|
||||
}
|
||||
}
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,14 +1,25 @@
|
||||
{
|
||||
"name": "everything-claude-code",
|
||||
"name": "ecc-universal",
|
||||
"version": "1.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ecc-universal",
|
||||
"version": "1.4.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"ecc-install": "install.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"eslint": "^9.39.2",
|
||||
"globals": "^17.1.0",
|
||||
"markdownlint-cli": "^0.47.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
@@ -294,6 +305,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -599,6 +611,7 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -1930,6 +1943,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- Pair programming and code generation
|
||||
- Worker agents in multi-agent systems
|
||||
|
||||
**Sonnet 4.5** (Best coding model):
|
||||
**Sonnet 4.6** (Best coding model):
|
||||
- Main development work
|
||||
- Orchestrating multi-agent workflows
|
||||
- Complex coding tasks
|
||||
|
||||
330
scripts/codemaps/generate.ts
Normal file
330
scripts/codemaps/generate.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* scripts/codemaps/generate.ts
|
||||
*
|
||||
* Codemap Generator for everything-claude-code (ECC)
|
||||
*
|
||||
* Scans the current working directory and generates architectural
|
||||
* codemap documentation under docs/CODEMAPS/ as specified by the
|
||||
* doc-updater agent.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/codemaps/generate.ts [srcDir]
|
||||
*
|
||||
* Output:
|
||||
* docs/CODEMAPS/INDEX.md
|
||||
* docs/CODEMAPS/frontend.md
|
||||
* docs/CODEMAPS/backend.md
|
||||
* docs/CODEMAPS/database.md
|
||||
* docs/CODEMAPS/integrations.md
|
||||
* docs/CODEMAPS/workers.md
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const SRC_DIR = process.argv[2] ? path.resolve(process.argv[2]) : ROOT;
|
||||
const OUTPUT_DIR = path.join(ROOT, 'docs', 'CODEMAPS');
|
||||
const TODAY = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Patterns used to classify files into codemap areas
|
||||
const AREA_PATTERNS: Record<string, RegExp[]> = {
|
||||
frontend: [
|
||||
/\/(app|pages|components|hooks|contexts|ui|views|layouts|styles)\//i,
|
||||
/\.(tsx|jsx|css|scss|sass|less|vue|svelte)$/i,
|
||||
],
|
||||
backend: [
|
||||
/\/(api|routes|controllers|middleware|server|services|handlers)\//i,
|
||||
/\.(route|controller|handler|middleware|service)\.(ts|js)$/i,
|
||||
],
|
||||
database: [
|
||||
/\/(models|schemas|migrations|prisma|drizzle|db|database|repositories)\//i,
|
||||
/\.(model|schema|migration|seed)\.(ts|js)$/i,
|
||||
/prisma\/schema\.prisma$/,
|
||||
/schema\.sql$/,
|
||||
],
|
||||
integrations: [
|
||||
/\/(integrations?|third-party|external|plugins?|adapters?|connectors?)\//i,
|
||||
/\.(integration|adapter|connector)\.(ts|js)$/i,
|
||||
],
|
||||
workers: [
|
||||
/\/(workers?|jobs?|queues?|tasks?|cron|background)\//i,
|
||||
/\.(worker|job|queue|task|cron)\.(ts|js)$/i,
|
||||
],
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File System Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Recursively collect all files under a directory, skipping common noise dirs. */
|
||||
function walkDir(dir: string, results: string[] = []): string[] {
|
||||
const SKIP = new Set([
|
||||
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build', 'out',
|
||||
'.turbo', 'coverage', '.cache', '__pycache__', '.venv', 'venv',
|
||||
]);
|
||||
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (SKIP.has(entry.name)) continue;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(fullPath, results);
|
||||
} else if (entry.isFile()) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Return path relative to ROOT, always using forward slashes. */
|
||||
function rel(p: string): string {
|
||||
return path.relative(ROOT, p).replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Analysis
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface AreaInfo {
|
||||
name: string;
|
||||
files: string[];
|
||||
entryPoints: string[];
|
||||
directories: string[];
|
||||
}
|
||||
|
||||
function classifyFiles(allFiles: string[]): Record<string, AreaInfo> {
|
||||
const areas: Record<string, AreaInfo> = {
|
||||
frontend: { name: 'Frontend', files: [], entryPoints: [], directories: [] },
|
||||
backend: { name: 'Backend/API', files: [], entryPoints: [], directories: [] },
|
||||
database: { name: 'Database', files: [], entryPoints: [], directories: [] },
|
||||
integrations: { name: 'Integrations', files: [], entryPoints: [], directories: [] },
|
||||
workers: { name: 'Workers', files: [], entryPoints: [], directories: [] },
|
||||
};
|
||||
|
||||
for (const file of allFiles) {
|
||||
const relPath = rel(file);
|
||||
for (const [area, patterns] of Object.entries(AREA_PATTERNS)) {
|
||||
if (patterns.some((p) => p.test(relPath))) {
|
||||
areas[area].files.push(relPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Derive unique directories and entry points per area
|
||||
for (const area of Object.values(areas)) {
|
||||
const dirs = new Set(area.files.map((f) => path.dirname(f)));
|
||||
area.directories = [...dirs].sort();
|
||||
|
||||
area.entryPoints = area.files
|
||||
.filter((f) => /index\.(ts|tsx|js|jsx)$/.test(f) || /main\.(ts|tsx|js|jsx)$/.test(f))
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
return areas;
|
||||
}
|
||||
|
||||
/** Count lines in a file (returns 0 on error). */
|
||||
function lineCount(p: string): number {
|
||||
try {
|
||||
const content = fs.readFileSync(p, 'utf8');
|
||||
return content.split('\n').length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a simple directory tree ASCII diagram (max 3 levels deep). */
|
||||
function buildTree(dir: string, prefix = '', depth = 0): string {
|
||||
if (depth > 2) return '';
|
||||
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage']);
|
||||
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dirs = entries.filter((e) => e.isDirectory() && !SKIP.has(e.name));
|
||||
const files = entries.filter((e) => e.isFile());
|
||||
|
||||
let result = '';
|
||||
const items = [...dirs, ...files];
|
||||
items.forEach((entry, i) => {
|
||||
const isLast = i === items.length - 1;
|
||||
const connector = isLast ? '└── ' : '├── ';
|
||||
result += `${prefix}${connector}${entry.name}\n`;
|
||||
if (entry.isDirectory()) {
|
||||
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
||||
result += buildTree(path.join(dir, entry.name), newPrefix, depth + 1);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Markdown Generators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function generateAreaDoc(areaKey: string, area: AreaInfo, allFiles: string[]): string {
|
||||
const fileCount = area.files.length;
|
||||
const totalLines = area.files.reduce((sum, f) => sum + lineCount(path.join(ROOT, f)), 0);
|
||||
|
||||
const entrySection = area.entryPoints.length > 0
|
||||
? area.entryPoints.map((e) => `- \`${e}\``).join('\n')
|
||||
: '- *(no index/main entry points detected)*';
|
||||
|
||||
const dirSection = area.directories.slice(0, 20)
|
||||
.map((d) => `- \`${d}/\``)
|
||||
.join('\n') || '- *(no dedicated directories detected)*';
|
||||
|
||||
const fileSection = area.files.slice(0, 30)
|
||||
.map((f) => `| \`${f}\` | ${lineCount(path.join(ROOT, f))} |`)
|
||||
.join('\n');
|
||||
|
||||
const moreFiles = area.files.length > 30
|
||||
? `\n*...and ${area.files.length - 30} more files*`
|
||||
: '';
|
||||
|
||||
return `# ${area.name} Codemap
|
||||
|
||||
**Last Updated:** ${TODAY}
|
||||
**Total Files:** ${fileCount}
|
||||
**Total Lines:** ${totalLines}
|
||||
|
||||
## Entry Points
|
||||
|
||||
${entrySection}
|
||||
|
||||
## Architecture
|
||||
|
||||
\`\`\`
|
||||
${area.name} Directory Structure
|
||||
${dirSection.replace(/- `/g, '').replace(/`\/$/gm, '/')}
|
||||
\`\`\`
|
||||
|
||||
## Key Modules
|
||||
|
||||
| File | Lines |
|
||||
|------|-------|
|
||||
${fileSection}${moreFiles}
|
||||
|
||||
## Data Flow
|
||||
|
||||
> Detected from file patterns. Review individual files for detailed data flow.
|
||||
|
||||
## External Dependencies
|
||||
|
||||
> Run \`npx jsdoc2md src/**/*.ts\` to extract JSDoc and identify external dependencies.
|
||||
|
||||
## Related Areas
|
||||
|
||||
- [INDEX](./INDEX.md) — Full overview
|
||||
- [Frontend](./frontend.md)
|
||||
- [Backend/API](./backend.md)
|
||||
- [Database](./database.md)
|
||||
- [Integrations](./integrations.md)
|
||||
- [Workers](./workers.md)
|
||||
`;
|
||||
}
|
||||
|
||||
function generateIndex(areas: Record<string, AreaInfo>, allFiles: string[]): string {
|
||||
const totalFiles = allFiles.length;
|
||||
const areaRows = Object.entries(areas)
|
||||
.map(([key, area]) => `| [${area.name}](./${key}.md) | ${area.files.length} files | ${area.directories.slice(0, 3).map((d) => `\`${d}\``).join(', ') || '—'} |`)
|
||||
.join('\n');
|
||||
|
||||
const topLevelTree = buildTree(SRC_DIR);
|
||||
|
||||
return `# Codebase Overview — CODEMAPS Index
|
||||
|
||||
**Last Updated:** ${TODAY}
|
||||
**Root:** \`${rel(SRC_DIR) || '.'}\`
|
||||
**Total Files Scanned:** ${totalFiles}
|
||||
|
||||
## Areas
|
||||
|
||||
| Area | Size | Key Directories |
|
||||
|------|------|-----------------|
|
||||
${areaRows}
|
||||
|
||||
## Repository Structure
|
||||
|
||||
\`\`\`
|
||||
${rel(SRC_DIR) || path.basename(SRC_DIR)}/
|
||||
${topLevelTree}\`\`\`
|
||||
|
||||
## How to Regenerate
|
||||
|
||||
\`\`\`bash
|
||||
npx tsx scripts/codemaps/generate.ts # Regenerate codemaps
|
||||
npx madge --image graph.svg src/ # Dependency graph (requires graphviz)
|
||||
npx jsdoc2md src/**/*.ts # Extract JSDoc
|
||||
\`\`\`
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Frontend](./frontend.md) — UI components, pages, hooks
|
||||
- [Backend/API](./backend.md) — API routes, controllers, middleware
|
||||
- [Database](./database.md) — Models, schemas, migrations
|
||||
- [Integrations](./integrations.md) — External services & adapters
|
||||
- [Workers](./workers.md) — Background jobs, queues, cron tasks
|
||||
`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function main(): void {
|
||||
console.log(`[generate.ts] Scanning: ${SRC_DIR}`);
|
||||
console.log(`[generate.ts] Output: ${OUTPUT_DIR}`);
|
||||
|
||||
// Ensure output directory exists
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
// Walk the directory tree
|
||||
const allFiles = walkDir(SRC_DIR);
|
||||
console.log(`[generate.ts] Found ${allFiles.length} files`);
|
||||
|
||||
// Classify files into areas
|
||||
const areas = classifyFiles(allFiles);
|
||||
|
||||
// Generate INDEX.md
|
||||
const indexContent = generateIndex(areas, allFiles);
|
||||
const indexPath = path.join(OUTPUT_DIR, 'INDEX.md');
|
||||
fs.writeFileSync(indexPath, indexContent, 'utf8');
|
||||
console.log(`[generate.ts] Written: ${rel(indexPath)}`);
|
||||
|
||||
// Generate per-area codemaps
|
||||
for (const [key, area] of Object.entries(areas)) {
|
||||
const content = generateAreaDoc(key, area, allFiles);
|
||||
const outPath = path.join(OUTPUT_DIR, `${key}.md`);
|
||||
fs.writeFileSync(outPath, content, 'utf8');
|
||||
console.log(`[generate.ts] Written: ${rel(outPath)} (${area.files.length} files)`);
|
||||
}
|
||||
|
||||
console.log('\n[generate.ts] Done! Codemaps written to docs/CODEMAPS/');
|
||||
console.log('[generate.ts] Files generated:');
|
||||
console.log(' docs/CODEMAPS/INDEX.md');
|
||||
console.log(' docs/CODEMAPS/frontend.md');
|
||||
console.log(' docs/CODEMAPS/backend.md');
|
||||
console.log(' docs/CODEMAPS/database.md');
|
||||
console.log(' docs/CODEMAPS/integrations.md');
|
||||
console.log(' docs/CODEMAPS/workers.md');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -32,7 +32,8 @@ process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
data += chunk;
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
if (stdinData.length < MAX_STDIN) {
|
||||
stdinData += chunk;
|
||||
const remaining = MAX_STDIN - stdinData.length;
|
||||
stdinData += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
data += chunk;
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +29,7 @@ process.stdin.on('end', () => {
|
||||
|
||||
if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
|
||||
const content = readFile(filePath);
|
||||
if (!content) { process.stdout.write(data); return; }
|
||||
if (!content) { process.stdout.write(data); process.exit(0); }
|
||||
const lines = content.split('\n');
|
||||
const matches = [];
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
data += chunk;
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ process.stdin.setEncoding("utf8");
|
||||
|
||||
process.stdin.on("data", (chunk) => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
data += chunk;
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -109,7 +109,8 @@ process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
if (stdinData.length < MAX_STDIN) {
|
||||
stdinData += chunk;
|
||||
const remaining = MAX_STDIN - stdinData.length;
|
||||
stdinData += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ function setProjectPackageManager(pmName, projectDir = process.cwd()) {
|
||||
|
||||
// Allowed characters in script/binary names: alphanumeric, dash, underscore, dot, slash, @
|
||||
// This prevents shell metacharacter injection while allowing scoped packages (e.g., @scope/pkg)
|
||||
const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_.\/-]+$/;
|
||||
const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_./-]+$/;
|
||||
|
||||
/**
|
||||
* Get the command to run a script
|
||||
@@ -316,7 +316,7 @@ function getRunCommand(script, options = {}) {
|
||||
|
||||
// Allowed characters in arguments: alphanumeric, whitespace, dashes, dots, slashes,
|
||||
// equals, colons, commas, quotes, @. Rejects shell metacharacters like ; | & ` $ ( ) { } < > !
|
||||
const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_.\/:=,'"*+-]+$/;
|
||||
const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_./:=,'"*+-]+$/;
|
||||
|
||||
/**
|
||||
* Get the command to execute a package binary
|
||||
@@ -370,28 +370,31 @@ function escapeRegex(str) {
|
||||
function getCommandPattern(action) {
|
||||
const patterns = [];
|
||||
|
||||
if (action === 'dev') {
|
||||
// Trim spaces from action to handle leading/trailing whitespace gracefully
|
||||
const trimmedAction = action.trim();
|
||||
|
||||
if (trimmedAction === 'dev') {
|
||||
patterns.push(
|
||||
'npm run dev',
|
||||
'pnpm( run)? dev',
|
||||
'yarn dev',
|
||||
'bun run dev'
|
||||
);
|
||||
} else if (action === 'install') {
|
||||
} else if (trimmedAction === 'install') {
|
||||
patterns.push(
|
||||
'npm install',
|
||||
'pnpm install',
|
||||
'yarn( install)?',
|
||||
'bun install'
|
||||
);
|
||||
} else if (action === 'test') {
|
||||
} else if (trimmedAction === 'test') {
|
||||
patterns.push(
|
||||
'npm test',
|
||||
'pnpm test',
|
||||
'yarn test',
|
||||
'bun test'
|
||||
);
|
||||
} else if (action === 'build') {
|
||||
} else if (trimmedAction === 'build') {
|
||||
patterns.push(
|
||||
'npm run build',
|
||||
'pnpm( run)? build',
|
||||
@@ -400,7 +403,7 @@ function getCommandPattern(action) {
|
||||
);
|
||||
} else {
|
||||
// Generic run command — escape regex metacharacters in action
|
||||
const escaped = escapeRegex(action);
|
||||
const escaped = escapeRegex(trimmedAction);
|
||||
patterns.push(
|
||||
`npm run ${escaped}`,
|
||||
`pnpm( run)? ${escaped}`,
|
||||
|
||||
@@ -21,7 +21,7 @@ Patterns for controlling LLM API costs while maintaining quality. Combines model
|
||||
Automatically select cheaper models for simple tasks, reserving expensive models for complex ones.
|
||||
|
||||
```python
|
||||
MODEL_SONNET = "claude-sonnet-4-5-20250929"
|
||||
MODEL_SONNET = "claude-sonnet-4-6"
|
||||
MODEL_HAIKU = "claude-haiku-4-5-20251001"
|
||||
|
||||
_SONNET_TEXT_THRESHOLD = 10_000 # chars
|
||||
@@ -155,7 +155,7 @@ def process(text: str, config: Config, tracker: CostTracker) -> tuple[Result, Co
|
||||
| Model | Input ($/1M tokens) | Output ($/1M tokens) | Relative Cost |
|
||||
|-------|---------------------|----------------------|---------------|
|
||||
| Haiku 4.5 | $0.80 | $4.00 | 1x |
|
||||
| Sonnet 4.5 | $3.00 | $15.00 | ~4x |
|
||||
| Sonnet 4.6 | $3.00 | $15.00 | ~4x |
|
||||
| Opus 4.5 | $15.00 | $75.00 | ~19x |
|
||||
|
||||
## Best Practices
|
||||
|
||||
159
skills/search-first/SKILL.md
Normal file
159
skills/search-first/SKILL.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: search-first
|
||||
description: Research-before-coding workflow. Search for existing tools, libraries, and patterns before writing custom code. Invokes the researcher agent.
|
||||
---
|
||||
|
||||
# /search-first — Research Before You Code
|
||||
|
||||
Systematizes the "search for existing solutions before implementing" workflow.
|
||||
|
||||
## Trigger
|
||||
|
||||
Use this skill when:
|
||||
- Starting a new feature that likely has existing solutions
|
||||
- Adding a dependency or integration
|
||||
- The user asks "add X functionality" and you're about to write code
|
||||
- Before creating a new utility, helper, or abstraction
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 1. NEED ANALYSIS │
|
||||
│ Define what functionality is needed │
|
||||
│ Identify language/framework constraints │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 2. PARALLEL SEARCH (researcher agent) │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ npm / │ │ MCP / │ │ GitHub / │ │
|
||||
│ │ PyPI │ │ Skills │ │ Web │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 3. EVALUATE │
|
||||
│ Score candidates (functionality, maint, │
|
||||
│ community, docs, license, deps) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 4. DECIDE │
|
||||
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ │
|
||||
│ │ Adopt │ │ Extend │ │ Build │ │
|
||||
│ │ as-is │ │ /Wrap │ │ Custom │ │
|
||||
│ └─────────┘ └──────────┘ └─────────┘ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 5. IMPLEMENT │
|
||||
│ Install package / Configure MCP / │
|
||||
│ Write minimal custom code │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Signal | Action |
|
||||
|--------|--------|
|
||||
| Exact match, well-maintained, MIT/Apache | **Adopt** — install and use directly |
|
||||
| Partial match, good foundation | **Extend** — install + write thin wrapper |
|
||||
| Multiple weak matches | **Compose** — combine 2-3 small packages |
|
||||
| Nothing suitable found | **Build** — write custom, but informed by research |
|
||||
|
||||
## How to Use
|
||||
|
||||
### Quick Mode (inline)
|
||||
|
||||
Before writing a utility or adding functionality, mentally run through:
|
||||
|
||||
1. Is this a common problem? → Search npm/PyPI
|
||||
2. Is there an MCP for this? → Check `~/.claude/settings.json` and search
|
||||
3. Is there a skill for this? → Check `~/.claude/skills/`
|
||||
4. Is there a GitHub template? → Search GitHub
|
||||
|
||||
### Full Mode (agent)
|
||||
|
||||
For non-trivial functionality, launch the researcher agent:
|
||||
|
||||
```
|
||||
Task(subagent_type="general-purpose", prompt="
|
||||
Research existing tools for: [DESCRIPTION]
|
||||
Language/framework: [LANG]
|
||||
Constraints: [ANY]
|
||||
|
||||
Search: npm/PyPI, MCP servers, Claude Code skills, GitHub
|
||||
Return: Structured comparison with recommendation
|
||||
")
|
||||
```
|
||||
|
||||
## Search Shortcuts by Category
|
||||
|
||||
### Development Tooling
|
||||
- Linting → `eslint`, `ruff`, `textlint`, `markdownlint`
|
||||
- Formatting → `prettier`, `black`, `gofmt`
|
||||
- Testing → `jest`, `pytest`, `go test`
|
||||
- Pre-commit → `husky`, `lint-staged`, `pre-commit`
|
||||
|
||||
### AI/LLM Integration
|
||||
- Claude SDK → Context7 for latest docs
|
||||
- Prompt management → Check MCP servers
|
||||
- Document processing → `unstructured`, `pdfplumber`, `mammoth`
|
||||
|
||||
### Data & APIs
|
||||
- HTTP clients → `httpx` (Python), `ky`/`got` (Node)
|
||||
- Validation → `zod` (TS), `pydantic` (Python)
|
||||
- Database → Check for MCP servers first
|
||||
|
||||
### Content & Publishing
|
||||
- Markdown processing → `remark`, `unified`, `markdown-it`
|
||||
- Image optimization → `sharp`, `imagemin`
|
||||
|
||||
## Integration Points
|
||||
|
||||
### With planner agent
|
||||
The planner should invoke researcher before Phase 1 (Architecture Review):
|
||||
- Researcher identifies available tools
|
||||
- Planner incorporates them into the implementation plan
|
||||
- Avoids "reinventing the wheel" in the plan
|
||||
|
||||
### With architect agent
|
||||
The architect should consult researcher for:
|
||||
- Technology stack decisions
|
||||
- Integration pattern discovery
|
||||
- Existing reference architectures
|
||||
|
||||
### With iterative-retrieval skill
|
||||
Combine for progressive discovery:
|
||||
- Cycle 1: Broad search (npm, PyPI, MCP)
|
||||
- Cycle 2: Evaluate top candidates in detail
|
||||
- Cycle 3: Test compatibility with project constraints
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: "Add dead link checking"
|
||||
```
|
||||
Need: Check markdown files for broken links
|
||||
Search: npm "markdown dead link checker"
|
||||
Found: textlint-rule-no-dead-link (score: 9/10)
|
||||
Action: ADOPT — npm install textlint-rule-no-dead-link
|
||||
Result: Zero custom code, battle-tested solution
|
||||
```
|
||||
|
||||
### Example 2: "Add HTTP client wrapper"
|
||||
```
|
||||
Need: Resilient HTTP client with retries and timeout handling
|
||||
Search: npm "http client retry", PyPI "httpx retry"
|
||||
Found: got (Node) with retry plugin, httpx (Python) with built-in retry
|
||||
Action: ADOPT — use got/httpx directly with retry config
|
||||
Result: Zero custom code, production-proven libraries
|
||||
```
|
||||
|
||||
### Example 3: "Add config file linter"
|
||||
```
|
||||
Need: Validate project config files against a schema
|
||||
Search: npm "config linter schema", "json schema validator cli"
|
||||
Found: ajv-cli (score: 8/10)
|
||||
Action: ADOPT + EXTEND — install ajv-cli, write project-specific schema
|
||||
Result: 1 package + 1 schema file, no custom validation logic
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- **Jumping to code**: Writing a utility without checking if one exists
|
||||
- **Ignoring MCP**: Not checking if an MCP server already provides the capability
|
||||
- **Over-customizing**: Wrapping a library so heavily it loses its benefits
|
||||
- **Dependency bloat**: Installing a massive package for one small feature
|
||||
175
skills/skill-stocktake/SKILL.md
Normal file
175
skills/skill-stocktake/SKILL.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
description: "Use when auditing Claude skills and commands for quality. Supports Quick Scan (changed skills only) and Full Stocktake modes with sequential subagent batch evaluation."
|
||||
---
|
||||
|
||||
# skill-stocktake
|
||||
|
||||
Slash command (`/skill-stocktake`) that audits all Claude skills and commands using a quality checklist + AI holistic judgment. Supports two modes: Quick Scan for recently changed skills, and Full Stocktake for a complete review.
|
||||
|
||||
## Scope
|
||||
|
||||
The command targets the following paths **relative to the directory where it is invoked**:
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `~/.claude/skills/` | Global skills (all projects) |
|
||||
| `{cwd}/.claude/skills/` | Project-level skills (if the directory exists) |
|
||||
|
||||
**At the start of Phase 1, the command explicitly lists which paths were found and scanned.**
|
||||
|
||||
### Targeting a specific project
|
||||
|
||||
To include project-level skills, run from that project's root directory:
|
||||
|
||||
```bash
|
||||
cd ~/path/to/my-project
|
||||
/skill-stocktake
|
||||
```
|
||||
|
||||
If the project has no `.claude/skills/` directory, only global skills and commands are evaluated.
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Trigger | Duration |
|
||||
|------|---------|---------|
|
||||
| Quick Scan | `results.json` exists (default) | 5–10 min |
|
||||
| Full Stocktake | `results.json` absent, or `/skill-stocktake full` | 20–30 min |
|
||||
|
||||
**Results cache:** `~/.claude/skills/skill-stocktake/results.json`
|
||||
|
||||
## Quick Scan Flow
|
||||
|
||||
Re-evaluate only skills that have changed since the last run (5–10 min).
|
||||
|
||||
1. Read `~/.claude/skills/skill-stocktake/results.json`
|
||||
2. Run: `bash ~/.claude/skills/skill-stocktake/scripts/quick-diff.sh \
|
||||
~/.claude/skills/skill-stocktake/results.json`
|
||||
(Project dir is auto-detected from `$PWD/.claude/skills`; pass it explicitly only if needed)
|
||||
3. If output is `[]`: report "No changes since last run." and stop
|
||||
4. Re-evaluate only those changed files using the same Phase 2 criteria
|
||||
5. Carry forward unchanged skills from previous results
|
||||
6. Output only the diff
|
||||
7. Run: `bash ~/.claude/skills/skill-stocktake/scripts/save-results.sh \
|
||||
~/.claude/skills/skill-stocktake/results.json <<< "$EVAL_RESULTS"`
|
||||
|
||||
## Full Stocktake Flow
|
||||
|
||||
### Phase 1 — Inventory
|
||||
|
||||
Run: `bash ~/.claude/skills/skill-stocktake/scripts/scan.sh`
|
||||
|
||||
The script enumerates skill files, extracts frontmatter, and collects UTC mtimes.
|
||||
Project dir is auto-detected from `$PWD/.claude/skills`; pass it explicitly only if needed.
|
||||
Present the scan summary and inventory table from the script output:
|
||||
|
||||
```
|
||||
Scanning:
|
||||
✓ ~/.claude/skills/ (17 files)
|
||||
✗ {cwd}/.claude/skills/ (not found — global skills only)
|
||||
```
|
||||
|
||||
| Skill | 7d use | 30d use | Description |
|
||||
|-------|--------|---------|-------------|
|
||||
|
||||
### Phase 2 — Quality Evaluation
|
||||
|
||||
Launch a Task tool subagent (**Explore agent, model: opus**) with the full inventory and checklist.
|
||||
The subagent reads each skill, applies the checklist, and returns per-skill JSON:
|
||||
|
||||
`{ "verdict": "Keep"|"Improve"|"Update"|"Retire"|"Merge into [X]", "reason": "..." }`
|
||||
|
||||
**Chunk guidance:** Process ~20 skills per subagent invocation to keep context manageable. Save intermediate results to `results.json` (`status: "in_progress"`) after each chunk.
|
||||
|
||||
After all skills are evaluated: set `status: "completed"`, proceed to Phase 3.
|
||||
|
||||
**Resume detection:** If `status: "in_progress"` is found on startup, resume from the first unevaluated skill.
|
||||
|
||||
Each skill is evaluated against this checklist:
|
||||
|
||||
```
|
||||
- [ ] Content overlap with other skills checked
|
||||
- [ ] Overlap with MEMORY.md / CLAUDE.md checked
|
||||
- [ ] Freshness of technical references verified (use WebSearch if tool names / CLI flags / APIs are present)
|
||||
- [ ] Usage frequency considered
|
||||
```
|
||||
|
||||
Verdict criteria:
|
||||
|
||||
| Verdict | Meaning |
|
||||
|---------|---------|
|
||||
| Keep | Useful and current |
|
||||
| Improve | Worth keeping, but specific improvements needed |
|
||||
| Update | Referenced technology is outdated (verify with WebSearch) |
|
||||
| Retire | Low quality, stale, or cost-asymmetric |
|
||||
| Merge into [X] | Substantial overlap with another skill; name the merge target |
|
||||
|
||||
Evaluation is **holistic AI judgment** — not a numeric rubric. Guiding dimensions:
|
||||
- **Actionability**: code examples, commands, or steps that let you act immediately
|
||||
- **Scope fit**: name, trigger, and content are aligned; not too broad or narrow
|
||||
- **Uniqueness**: value not replaceable by MEMORY.md / CLAUDE.md / another skill
|
||||
- **Currency**: technical references work in the current environment
|
||||
|
||||
**Reason quality requirements** — the `reason` field must be self-contained and decision-enabling:
|
||||
- Do NOT write "unchanged" alone — always restate the core evidence
|
||||
- For **Retire**: state (1) what specific defect was found, (2) what covers the same need instead
|
||||
- Bad: `"Superseded"`
|
||||
- Good: `"disable-model-invocation: true already set; superseded by continuous-learning-v2 which covers all the same patterns plus confidence scoring. No unique content remains."`
|
||||
- For **Merge**: name the target and describe what content to integrate
|
||||
- Bad: `"Overlaps with X"`
|
||||
- Good: `"42-line thin content; Step 4 of chatlog-to-article already covers the same workflow. Integrate the 'article angle' tip as a note in that skill."`
|
||||
- For **Improve**: describe the specific change needed (what section, what action, target size if relevant)
|
||||
- Bad: `"Too long"`
|
||||
- Good: `"276 lines; Section 'Framework Comparison' (L80–140) duplicates ai-era-architecture-principles; delete it to reach ~150 lines."`
|
||||
- For **Keep** (mtime-only change in Quick Scan): restate the original verdict rationale, do not write "unchanged"
|
||||
- Bad: `"Unchanged"`
|
||||
- Good: `"mtime updated but content unchanged. Unique Python reference explicitly imported by rules/python/; no overlap found."`
|
||||
|
||||
### Phase 3 — Summary Table
|
||||
|
||||
| Skill | 7d use | Verdict | Reason |
|
||||
|-------|--------|---------|--------|
|
||||
|
||||
### Phase 4 — Consolidation
|
||||
|
||||
1. **Retire / Merge**: present detailed justification per file before confirming with user:
|
||||
- What specific problem was found (overlap, staleness, broken references, etc.)
|
||||
- What alternative covers the same functionality (for Retire: which existing skill/rule; for Merge: the target file and what content to integrate)
|
||||
- Impact of removal (any dependent skills, MEMORY.md references, or workflows affected)
|
||||
2. **Improve**: present specific improvement suggestions with rationale:
|
||||
- What to change and why (e.g., "trim 430→200 lines because sections X/Y duplicate python-patterns")
|
||||
- User decides whether to act
|
||||
3. **Update**: present updated content with sources checked
|
||||
4. Check MEMORY.md line count; propose compression if >100 lines
|
||||
|
||||
## Results File Schema
|
||||
|
||||
`~/.claude/skills/skill-stocktake/results.json`:
|
||||
|
||||
**`evaluated_at`**: Must be set to the actual UTC time of evaluation completion.
|
||||
Obtain via Bash: `date -u +%Y-%m-%dT%H:%M:%SZ`. Never use a date-only approximation like `T00:00:00Z`.
|
||||
|
||||
```json
|
||||
{
|
||||
"evaluated_at": "2026-02-21T10:00:00Z",
|
||||
"mode": "full",
|
||||
"batch_progress": {
|
||||
"total": 80,
|
||||
"evaluated": 80,
|
||||
"status": "completed"
|
||||
},
|
||||
"skills": {
|
||||
"skill-name": {
|
||||
"path": "~/.claude/skills/skill-name/SKILL.md",
|
||||
"verdict": "Keep",
|
||||
"reason": "Concrete, actionable, unique value for X workflow",
|
||||
"mtime": "2026-01-15T08:30:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Evaluation is blind: the same checklist applies to all skills regardless of origin (ECC, self-authored, auto-extracted)
|
||||
- Archive / delete operations always require explicit user confirmation
|
||||
- No verdict branching by skill origin
|
||||
87
skills/skill-stocktake/scripts/quick-diff.sh
Executable file
87
skills/skill-stocktake/scripts/quick-diff.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# quick-diff.sh — compare skill file mtimes against results.json evaluated_at
|
||||
# Usage: quick-diff.sh RESULTS_JSON [CWD_SKILLS_DIR]
|
||||
# Output: JSON array of changed/new files to stdout (empty [] if no changes)
|
||||
#
|
||||
# When CWD_SKILLS_DIR is omitted, defaults to $PWD/.claude/skills so the
|
||||
# script always picks up project-level skills without relying on the caller.
|
||||
#
|
||||
# Environment:
|
||||
# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only;
|
||||
# do not set in production — intended for bats tests)
|
||||
# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RESULTS_JSON="${1:-}"
|
||||
CWD_SKILLS_DIR="${SKILL_STOCKTAKE_PROJECT_DIR:-${2:-$PWD/.claude/skills}}"
|
||||
GLOBAL_DIR="${SKILL_STOCKTAKE_GLOBAL_DIR:-$HOME/.claude/skills}"
|
||||
|
||||
if [[ -z "$RESULTS_JSON" || ! -f "$RESULTS_JSON" ]]; then
|
||||
echo "Error: RESULTS_JSON not found: ${RESULTS_JSON:-<empty>}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate CWD_SKILLS_DIR looks like a .claude/skills path (defense-in-depth).
|
||||
# Only warn when the path exists — a nonexistent path poses no traversal risk.
|
||||
if [[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" && "$CWD_SKILLS_DIR" != */.claude/skills* ]]; then
|
||||
echo "Warning: CWD_SKILLS_DIR does not look like a .claude/skills path: $CWD_SKILLS_DIR" >&2
|
||||
fi
|
||||
|
||||
evaluated_at=$(jq -r '.evaluated_at' "$RESULTS_JSON")
|
||||
|
||||
# Fail fast on a missing or malformed evaluated_at rather than producing
|
||||
# unpredictable results from ISO 8601 string comparison against "null".
|
||||
if [[ ! "$evaluated_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then
|
||||
echo "Error: invalid or missing evaluated_at in $RESULTS_JSON: $evaluated_at" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pre-extract known paths from results.json once (O(1) lookup per file instead of O(n*m))
|
||||
known_paths=$(jq -r '.skills[].path' "$RESULTS_JSON" 2>/dev/null)
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
# Use a function to avoid embedding $tmpdir in a quoted string (prevents injection
|
||||
# if TMPDIR were crafted to contain shell metacharacters).
|
||||
_cleanup() { rm -rf "$tmpdir"; }
|
||||
trap _cleanup EXIT
|
||||
|
||||
# Shared counter across process_dir calls — intentionally NOT local
|
||||
i=0
|
||||
|
||||
process_dir() {
|
||||
local dir="$1"
|
||||
while IFS= read -r file; do
|
||||
local mtime dp is_new
|
||||
mtime=$(date -u -r "$file" +%Y-%m-%dT%H:%M:%SZ)
|
||||
dp="${file/#$HOME/~}"
|
||||
|
||||
# Check if this file is known to results.json (exact whole-line match to
|
||||
# avoid substring false-positives, e.g. "python-patterns" matching "python-patterns-v2").
|
||||
if echo "$known_paths" | grep -qxF "$dp"; then
|
||||
is_new="false"
|
||||
# Known file: only emit if mtime changed (ISO 8601 string comparison is safe)
|
||||
[[ "$mtime" > "$evaluated_at" ]] || continue
|
||||
else
|
||||
is_new="true"
|
||||
# New file: always emit regardless of mtime
|
||||
fi
|
||||
|
||||
jq -n \
|
||||
--arg path "$dp" \
|
||||
--arg mtime "$mtime" \
|
||||
--argjson is_new "$is_new" \
|
||||
'{path:$path,mtime:$mtime,is_new:$is_new}' \
|
||||
> "$tmpdir/$i.json"
|
||||
i=$((i+1))
|
||||
done < <(find "$dir" -name "*.md" -type f 2>/dev/null | sort)
|
||||
}
|
||||
|
||||
[[ -d "$GLOBAL_DIR" ]] && process_dir "$GLOBAL_DIR"
|
||||
[[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" ]] && process_dir "$CWD_SKILLS_DIR"
|
||||
|
||||
if [[ $i -eq 0 ]]; then
|
||||
echo "[]"
|
||||
else
|
||||
jq -s '.' "$tmpdir"/*.json
|
||||
fi
|
||||
56
skills/skill-stocktake/scripts/save-results.sh
Executable file
56
skills/skill-stocktake/scripts/save-results.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# save-results.sh — merge evaluated skills into results.json with correct UTC timestamp
|
||||
# Usage: save-results.sh RESULTS_JSON <<< "$EVAL_JSON"
|
||||
#
|
||||
# stdin format:
|
||||
# { "skills": {...}, "mode"?: "full"|"quick", "batch_progress"?: {...} }
|
||||
#
|
||||
# Always sets evaluated_at to current UTC time via `date -u`.
|
||||
# Merges stdin .skills into existing results.json (new entries override old).
|
||||
# Optionally updates .mode and .batch_progress if present in stdin.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RESULTS_JSON="${1:-}"
|
||||
|
||||
if [[ -z "$RESULTS_JSON" ]]; then
|
||||
echo "Error: RESULTS_JSON argument required" >&2
|
||||
echo "Usage: save-results.sh RESULTS_JSON <<< \"\$EVAL_JSON\"" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EVALUATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# Read eval results from stdin and validate JSON before touching the results file
|
||||
input_json=$(cat)
|
||||
if ! echo "$input_json" | jq empty 2>/dev/null; then
|
||||
echo "Error: stdin is not valid JSON" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$RESULTS_JSON" ]]; then
|
||||
# Bootstrap: create new results.json from stdin JSON + current UTC timestamp
|
||||
echo "$input_json" | jq --arg ea "$EVALUATED_AT" \
|
||||
'. + { evaluated_at: $ea }' > "$RESULTS_JSON"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Merge: new .skills override existing ones; old skills not in input_json are kept.
|
||||
# Optionally update .mode and .batch_progress if provided.
|
||||
#
|
||||
# Use mktemp for a collision-safe temp file (concurrent runs on the same RESULTS_JSON
|
||||
# would race on a predictable ".tmp" suffix; random suffix prevents silent overwrites).
|
||||
tmp=$(mktemp "${RESULTS_JSON}.XXXXXX")
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
jq -s \
|
||||
--arg ea "$EVALUATED_AT" \
|
||||
'.[0] as $existing | .[1] as $new |
|
||||
$existing |
|
||||
.evaluated_at = $ea |
|
||||
.skills = ($existing.skills + ($new.skills // {})) |
|
||||
if ($new | has("mode")) then .mode = $new.mode else . end |
|
||||
if ($new | has("batch_progress")) then .batch_progress = $new.batch_progress else . end' \
|
||||
"$RESULTS_JSON" <(echo "$input_json") > "$tmp"
|
||||
|
||||
mv "$tmp" "$RESULTS_JSON"
|
||||
170
skills/skill-stocktake/scripts/scan.sh
Executable file
170
skills/skill-stocktake/scripts/scan.sh
Executable file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env bash
|
||||
# scan.sh — enumerate skill files, extract frontmatter and UTC mtime
|
||||
# Usage: scan.sh [CWD_SKILLS_DIR]
|
||||
# Output: JSON to stdout
|
||||
#
|
||||
# When CWD_SKILLS_DIR is omitted, defaults to $PWD/.claude/skills so the
|
||||
# script always picks up project-level skills without relying on the caller.
|
||||
#
|
||||
# Environment:
|
||||
# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only;
|
||||
# do not set in production — intended for bats tests)
|
||||
# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GLOBAL_DIR="${SKILL_STOCKTAKE_GLOBAL_DIR:-$HOME/.claude/skills}"
|
||||
CWD_SKILLS_DIR="${SKILL_STOCKTAKE_PROJECT_DIR:-${1:-$PWD/.claude/skills}}"
|
||||
# Path to JSONL file containing tool-use observations (optional; used for usage frequency counts).
|
||||
# Override via SKILL_STOCKTAKE_OBSERVATIONS env var if your setup uses a different path.
|
||||
OBSERVATIONS="${SKILL_STOCKTAKE_OBSERVATIONS:-$HOME/.claude/observations.jsonl}"
|
||||
|
||||
# Validate CWD_SKILLS_DIR looks like a .claude/skills path (defense-in-depth).
|
||||
# Only warn when the path exists — a nonexistent path poses no traversal risk.
|
||||
if [[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" && "$CWD_SKILLS_DIR" != */.claude/skills* ]]; then
|
||||
echo "Warning: CWD_SKILLS_DIR does not look like a .claude/skills path: $CWD_SKILLS_DIR" >&2
|
||||
fi
|
||||
|
||||
# Extract a frontmatter field (handles both quoted and unquoted single-line values).
|
||||
# Does NOT support multi-line YAML blocks (| or >) or nested YAML keys.
|
||||
extract_field() {
|
||||
local file="$1" field="$2"
|
||||
awk -v f="$field" '
|
||||
BEGIN { fm=0 }
|
||||
/^---$/ { fm++; next }
|
||||
fm==1 {
|
||||
n = length(f) + 2
|
||||
if (substr($0, 1, n) == f ": ") {
|
||||
val = substr($0, n+1)
|
||||
gsub(/^"/, "", val)
|
||||
gsub(/"$/, "", val)
|
||||
print val
|
||||
exit
|
||||
}
|
||||
}
|
||||
fm>=2 { exit }
|
||||
' "$file"
|
||||
}
|
||||
|
||||
# Get UTC timestamp N days ago (supports both macOS and GNU date)
|
||||
date_ago() {
|
||||
local n="$1"
|
||||
date -u -v-"${n}d" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null ||
|
||||
date -u -d "${n} days ago" +%Y-%m-%dT%H:%M:%SZ
|
||||
}
|
||||
|
||||
# Count observations matching a file path since a cutoff timestamp
|
||||
count_obs() {
|
||||
local file="$1" cutoff="$2"
|
||||
if [[ ! -f "$OBSERVATIONS" ]]; then
|
||||
echo 0
|
||||
return
|
||||
fi
|
||||
jq -r --arg p "$file" --arg c "$cutoff" \
|
||||
'select(.tool=="Read" and .path==$p and .timestamp>=$c) | 1' \
|
||||
"$OBSERVATIONS" 2>/dev/null | wc -l | tr -d ' '
|
||||
}
|
||||
|
||||
# Scan a directory and produce a JSON array of skill objects
|
||||
scan_dir_to_json() {
|
||||
local dir="$1"
|
||||
local c7 c30
|
||||
c7=$(date_ago 7)
|
||||
c30=$(date_ago 30)
|
||||
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d)
|
||||
# Use a function to avoid embedding $tmpdir in a quoted string (prevents injection
|
||||
# if TMPDIR were crafted to contain shell metacharacters).
|
||||
local _scan_tmpdir="$tmpdir"
|
||||
_scan_cleanup() { rm -rf "$_scan_tmpdir"; }
|
||||
trap _scan_cleanup RETURN
|
||||
|
||||
# Pre-aggregate observation counts in two passes (one per window) instead of
|
||||
# calling jq per-file — reduces from O(n*m) to O(n+m) jq invocations.
|
||||
local obs_7d_counts obs_30d_counts
|
||||
obs_7d_counts=""
|
||||
obs_30d_counts=""
|
||||
if [[ -f "$OBSERVATIONS" ]]; then
|
||||
obs_7d_counts=$(jq -r --arg c "$c7" \
|
||||
'select(.tool=="Read" and .timestamp>=$c) | .path' \
|
||||
"$OBSERVATIONS" 2>/dev/null | sort | uniq -c)
|
||||
obs_30d_counts=$(jq -r --arg c "$c30" \
|
||||
'select(.tool=="Read" and .timestamp>=$c) | .path' \
|
||||
"$OBSERVATIONS" 2>/dev/null | sort | uniq -c)
|
||||
fi
|
||||
|
||||
local i=0
|
||||
while IFS= read -r file; do
|
||||
local name desc mtime u7 u30 dp
|
||||
name=$(extract_field "$file" "name")
|
||||
desc=$(extract_field "$file" "description")
|
||||
mtime=$(date -u -r "$file" +%Y-%m-%dT%H:%M:%SZ)
|
||||
# Use awk exact field match to avoid substring false-positives from grep -F.
|
||||
# uniq -c output format: " N /path/to/file" — path is always field 2.
|
||||
u7=$(echo "$obs_7d_counts" | awk -v f="$file" '$2 == f {print $1}' | head -1)
|
||||
u7="${u7:-0}"
|
||||
u30=$(echo "$obs_30d_counts" | awk -v f="$file" '$2 == f {print $1}' | head -1)
|
||||
u30="${u30:-0}"
|
||||
dp="${file/#$HOME/~}"
|
||||
|
||||
jq -n \
|
||||
--arg path "$dp" \
|
||||
--arg name "$name" \
|
||||
--arg description "$desc" \
|
||||
--arg mtime "$mtime" \
|
||||
--argjson use_7d "$u7" \
|
||||
--argjson use_30d "$u30" \
|
||||
'{path:$path,name:$name,description:$description,use_7d:$use_7d,use_30d:$use_30d,mtime:$mtime}' \
|
||||
> "$tmpdir/$i.json"
|
||||
i=$((i+1))
|
||||
done < <(find "$dir" -name "*.md" -type f 2>/dev/null | sort)
|
||||
|
||||
if [[ $i -eq 0 ]]; then
|
||||
echo "[]"
|
||||
else
|
||||
jq -s '.' "$tmpdir"/*.json
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
||||
global_found="false"
|
||||
global_count=0
|
||||
global_skills="[]"
|
||||
|
||||
if [[ -d "$GLOBAL_DIR" ]]; then
|
||||
global_found="true"
|
||||
global_skills=$(scan_dir_to_json "$GLOBAL_DIR")
|
||||
global_count=$(echo "$global_skills" | jq 'length')
|
||||
fi
|
||||
|
||||
project_found="false"
|
||||
project_path=""
|
||||
project_count=0
|
||||
project_skills="[]"
|
||||
|
||||
if [[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" ]]; then
|
||||
project_found="true"
|
||||
project_path="$CWD_SKILLS_DIR"
|
||||
project_skills=$(scan_dir_to_json "$CWD_SKILLS_DIR")
|
||||
project_count=$(echo "$project_skills" | jq 'length')
|
||||
fi
|
||||
|
||||
# Merge global + project skills into one array
|
||||
all_skills=$(jq -s 'add' <(echo "$global_skills") <(echo "$project_skills"))
|
||||
|
||||
jq -n \
|
||||
--arg global_found "$global_found" \
|
||||
--argjson global_count "$global_count" \
|
||||
--arg project_found "$project_found" \
|
||||
--arg project_path "$project_path" \
|
||||
--argjson project_count "$project_count" \
|
||||
--argjson skills "$all_skills" \
|
||||
'{
|
||||
scan_summary: {
|
||||
global: { found: ($global_found == "true"), count: $global_count },
|
||||
project: { found: ($project_found == "true"), path: $project_path, count: $project_count }
|
||||
},
|
||||
skills: $skills
|
||||
}'
|
||||
@@ -11,7 +11,7 @@ const assert = require('assert');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { spawnSync, execFileSync } = require('child_process');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const evaluateScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'evaluate-session.js');
|
||||
|
||||
|
||||
@@ -1324,7 +1324,7 @@ async function runTests() {
|
||||
val = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(val, 2, 'Second call should write count 2');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1341,7 +1341,7 @@ async function runTests() {
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.ok(result.stderr.includes('5 tool calls reached'), 'Should suggest compact at threshold');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1359,7 +1359,7 @@ async function runTests() {
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.ok(result.stderr.includes('30 tool calls'), 'Should suggest at threshold + 25n intervals');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1376,7 +1376,7 @@ async function runTests() {
|
||||
assert.ok(!result.stderr.includes('tool calls reached'), 'Should not suggest below threshold');
|
||||
assert.ok(!result.stderr.includes('checkpoint'), 'Should not suggest checkpoint');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1394,7 +1394,7 @@ async function runTests() {
|
||||
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(newCount, 1, 'Should reset to 1 on overflow value');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1410,7 +1410,7 @@ async function runTests() {
|
||||
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(newCount, 1, 'Should reset to 1 on negative value');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1426,7 +1426,7 @@ async function runTests() {
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.ok(result.stderr.includes('50 tool calls reached'), 'Zero threshold should fall back to 50');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1443,7 +1443,7 @@ async function runTests() {
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.ok(result.stderr.includes('50 tool calls reached'), 'Should use default threshold of 50');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1883,7 +1883,7 @@ async function runTests() {
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.ok(result.stderr.includes('38 tool calls'), 'Should suggest at threshold(13) + 25 = 38');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1901,7 +1901,7 @@ async function runTests() {
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.ok(!result.stderr.includes('checkpoint'), 'Should NOT suggest at count=50 with threshold=13');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1918,7 +1918,7 @@ async function runTests() {
|
||||
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(newCount, 1, 'Should reset to 1 on corrupted file content');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -1935,7 +1935,7 @@ async function runTests() {
|
||||
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(newCount, 1000001, 'Should increment from exactly 1000000');
|
||||
} finally {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ const compactScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'sugg
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${err.message}`);
|
||||
} catch (_err) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${_err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,11 @@ function runTests() {
|
||||
|
||||
// Cleanup helper
|
||||
function cleanupCounter() {
|
||||
try { fs.unlinkSync(counterFile); } catch {}
|
||||
try {
|
||||
fs.unlinkSync(counterFile);
|
||||
} catch (_err) {
|
||||
// Ignore error
|
||||
}
|
||||
}
|
||||
|
||||
// Basic functionality
|
||||
@@ -80,7 +84,8 @@ function runTests() {
|
||||
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(count, 1, 'Counter should be 1 after first run');
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('increments counter on subsequent runs', () => {
|
||||
cleanupCounter();
|
||||
@@ -90,7 +95,8 @@ function runTests() {
|
||||
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(count, 3, 'Counter should be 3 after three runs');
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// Threshold suggestion
|
||||
console.log('\nThreshold suggestion:');
|
||||
@@ -106,7 +112,8 @@ function runTests() {
|
||||
`Should suggest compact at threshold. Got stderr: ${result.stderr}`
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('does NOT suggest compact before threshold', () => {
|
||||
cleanupCounter();
|
||||
@@ -117,7 +124,8 @@ function runTests() {
|
||||
'Should NOT suggest compact before threshold'
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// Interval suggestion (every 25 calls after threshold)
|
||||
console.log('\nInterval suggestion:');
|
||||
@@ -135,7 +143,8 @@ function runTests() {
|
||||
`Should suggest at threshold+25 interval. Got stderr: ${result.stderr}`
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// Environment variable handling
|
||||
console.log('\nEnvironment variable handling:');
|
||||
@@ -151,7 +160,8 @@ function runTests() {
|
||||
`Should use default threshold of 50. Got stderr: ${result.stderr}`
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('ignores invalid COMPACT_THRESHOLD (negative)', () => {
|
||||
cleanupCounter();
|
||||
@@ -163,7 +173,8 @@ function runTests() {
|
||||
`Should fallback to 50 for negative threshold. Got stderr: ${result.stderr}`
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('ignores non-numeric COMPACT_THRESHOLD', () => {
|
||||
cleanupCounter();
|
||||
@@ -175,7 +186,8 @@ function runTests() {
|
||||
`Should fallback to 50 for non-numeric threshold. Got stderr: ${result.stderr}`
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// Corrupted counter file
|
||||
console.log('\nCorrupted counter file:');
|
||||
@@ -189,7 +201,8 @@ function runTests() {
|
||||
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(count, 1, 'Should reset to 1 on corrupted file');
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('resets counter on extremely large value', () => {
|
||||
cleanupCounter();
|
||||
@@ -200,7 +213,8 @@ function runTests() {
|
||||
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(count, 1, 'Should reset to 1 for value > 1000000');
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('handles empty counter file', () => {
|
||||
cleanupCounter();
|
||||
@@ -211,7 +225,8 @@ function runTests() {
|
||||
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(count, 1, 'Should start at 1 for empty file');
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// Session isolation
|
||||
console.log('\nSession isolation:');
|
||||
@@ -230,10 +245,11 @@ function runTests() {
|
||||
assert.strictEqual(countA, 2, 'Session A should have count 2');
|
||||
assert.strictEqual(countB, 1, 'Session B should have count 1');
|
||||
} finally {
|
||||
try { fs.unlinkSync(fileA); } catch {}
|
||||
try { fs.unlinkSync(fileB); } catch {}
|
||||
try { fs.unlinkSync(fileA); } catch (_err) { /* ignore */ }
|
||||
try { fs.unlinkSync(fileB); } catch (_err) { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// Always exits 0
|
||||
console.log('\nExit code:');
|
||||
@@ -243,7 +259,8 @@ function runTests() {
|
||||
const result = runCompact({ CLAUDE_SESSION_ID: testSession });
|
||||
assert.strictEqual(result.code, 0, 'Should always exit 0');
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// ── Round 29: threshold boundary values ──
|
||||
console.log('\nThreshold boundary values:');
|
||||
@@ -258,7 +275,8 @@ function runTests() {
|
||||
`Should fallback to 50 for threshold=0. Got stderr: ${result.stderr}`
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('accepts COMPACT_THRESHOLD=10000 (boundary max)', () => {
|
||||
cleanupCounter();
|
||||
@@ -270,7 +288,8 @@ function runTests() {
|
||||
`Should accept threshold=10000. Got stderr: ${result.stderr}`
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('rejects COMPACT_THRESHOLD=10001 (falls back to 50)', () => {
|
||||
cleanupCounter();
|
||||
@@ -282,7 +301,8 @@ function runTests() {
|
||||
`Should fallback to 50 for threshold=10001. Got stderr: ${result.stderr}`
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('rejects float COMPACT_THRESHOLD (e.g. 3.5)', () => {
|
||||
cleanupCounter();
|
||||
@@ -297,33 +317,36 @@ function runTests() {
|
||||
'Float threshold should be parseInt-ed to 3, no suggestion at count=50'
|
||||
);
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('counter value at exact boundary 1000000 is valid', () => {
|
||||
cleanupCounter();
|
||||
fs.writeFileSync(counterFile, '999999');
|
||||
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' });
|
||||
runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' });
|
||||
// 999999 is valid (> 0, <= 1000000), count becomes 1000000
|
||||
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(count, 1000000, 'Counter at 1000000 boundary should be valid');
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('counter value at 1000001 is clamped (reset to 1)', () => {
|
||||
cleanupCounter();
|
||||
fs.writeFileSync(counterFile, '1000001');
|
||||
const result = runCompact({ CLAUDE_SESSION_ID: testSession });
|
||||
runCompact({ CLAUDE_SESSION_ID: testSession });
|
||||
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(count, 1, 'Counter > 1000000 should be reset to 1');
|
||||
cleanupCounter();
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// ── Round 64: default session ID fallback ──
|
||||
console.log('\nDefault session ID fallback (Round 64):');
|
||||
|
||||
if (test('uses "default" session ID when CLAUDE_SESSION_ID is empty', () => {
|
||||
const defaultCounterFile = getCounterFilePath('default');
|
||||
try { fs.unlinkSync(defaultCounterFile); } catch {}
|
||||
try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ }
|
||||
try {
|
||||
// Pass empty CLAUDE_SESSION_ID — falsy, so script uses 'default'
|
||||
const env = { ...process.env, CLAUDE_SESSION_ID: '' };
|
||||
@@ -338,12 +361,14 @@ function runTests() {
|
||||
const count = parseInt(fs.readFileSync(defaultCounterFile, 'utf8').trim(), 10);
|
||||
assert.strictEqual(count, 1, 'Counter should be 1 for first run with default session');
|
||||
} finally {
|
||||
try { fs.unlinkSync(defaultCounterFile); } catch {}
|
||||
try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ }
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// Summary
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
console.log(`
|
||||
Results: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -262,8 +262,13 @@ async function runTests() {
|
||||
});
|
||||
});
|
||||
|
||||
assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
|
||||
assert.strictEqual(code, 2, 'Blocking hook should exit with code 2');
|
||||
// Hook only blocks on non-Windows platforms (tmux is Unix-only)
|
||||
if (process.platform === 'win32') {
|
||||
assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)');
|
||||
} else {
|
||||
assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
|
||||
assert.strictEqual(code, 2, 'Blocking hook should exit with code 2');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ==========================================
|
||||
@@ -298,7 +303,12 @@ async function runTests() {
|
||||
});
|
||||
});
|
||||
|
||||
assert.strictEqual(code, 2, 'Blocking hook should exit 2');
|
||||
// Hook only blocks on non-Windows platforms (tmux is Unix-only)
|
||||
if (process.platform === 'win32') {
|
||||
assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)');
|
||||
} else {
|
||||
assert.strictEqual(code, 2, 'Blocking hook should exit 2');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('hooks handle missing files gracefully', async () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -787,7 +787,6 @@ function runTests() {
|
||||
// Verify the file exists
|
||||
const aliasesPath = path.join(tmpHome, '.claude', 'session-aliases.json');
|
||||
assert.ok(fs.existsSync(aliasesPath), 'Aliases file should exist');
|
||||
const contentBefore = fs.readFileSync(aliasesPath, 'utf8');
|
||||
|
||||
// Attempt to save circular data — will fail
|
||||
const circular = { aliases: {}, metadata: {} };
|
||||
|
||||
@@ -1124,7 +1124,7 @@ src/main.ts
|
||||
} else {
|
||||
delete process.env.USERPROFILE;
|
||||
}
|
||||
try { fs.rmSync(r33Home, { recursive: true, force: true }); } catch {}
|
||||
try { fs.rmSync(r33Home, { recursive: true, force: true }); } catch (_e) { /* ignore cleanup errors */ }
|
||||
|
||||
// ── Round 46: path heuristic and checklist edge cases ──
|
||||
console.log('\ngetSessionStats Windows path heuristic (Round 46):');
|
||||
@@ -1488,6 +1488,27 @@ src/main.ts
|
||||
'Content without session items should have 0 totalItems');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Re-establish test environment for Rounds 95-98 (these tests need sessions to exist)
|
||||
const tmpHome2 = path.join(os.tmpdir(), `ecc-session-mgr-test-2-${Date.now()}`);
|
||||
const tmpSessionsDir2 = path.join(tmpHome2, '.claude', 'sessions');
|
||||
fs.mkdirSync(tmpSessionsDir2, { recursive: true });
|
||||
const origHome2 = process.env.HOME;
|
||||
const origUserProfile2 = process.env.USERPROFILE;
|
||||
|
||||
// Create test session files for these tests
|
||||
const testSessions2 = [
|
||||
{ name: '2026-01-15-aaaa1111-session.tmp', content: '# Test Session 1' },
|
||||
{ name: '2026-02-01-bbbb2222-session.tmp', content: '# Test Session 2' },
|
||||
{ name: '2026-02-10-cccc3333-session.tmp', content: '# Test Session 3' },
|
||||
];
|
||||
for (const session of testSessions2) {
|
||||
const filePath = path.join(tmpSessionsDir2, session.name);
|
||||
fs.writeFileSync(filePath, session.content);
|
||||
}
|
||||
|
||||
process.env.HOME = tmpHome2;
|
||||
process.env.USERPROFILE = tmpHome2;
|
||||
|
||||
// ── Round 95: getAllSessions with both negative offset AND negative limit ──
|
||||
console.log('\nRound 95: getAllSessions (both negative offset and negative limit):');
|
||||
|
||||
@@ -1579,6 +1600,20 @@ src/main.ts
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Cleanup test environment for Rounds 95-98 that needed sessions
|
||||
// (Round 98: parseSessionFilename below doesn't need sessions)
|
||||
process.env.HOME = origHome2;
|
||||
if (origUserProfile2 !== undefined) {
|
||||
process.env.USERPROFILE = origUserProfile2;
|
||||
} else {
|
||||
delete process.env.USERPROFILE;
|
||||
}
|
||||
try {
|
||||
fs.rmSync(tmpHome2, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
|
||||
// ── Round 98: parseSessionFilename with null input throws TypeError ──
|
||||
console.log('\nRound 98: parseSessionFilename (null input — crashes at line 30):');
|
||||
|
||||
@@ -1985,7 +2020,7 @@ file.ts
|
||||
assert.ok(!afterContent.includes('Appended data'),
|
||||
'Original content should be unchanged');
|
||||
} finally {
|
||||
try { fs.chmodSync(readOnlyFile, 0o644); } catch {}
|
||||
try { fs.chmodSync(readOnlyFile, 0o644); } catch (_e) { /* ignore permission errors */ }
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
@@ -2329,6 +2364,7 @@ file.ts
|
||||
if (test('getSessionById matches old format YYYY-MM-DD-session.tmp via noIdMatch path', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'r122-old-format-'));
|
||||
const origHome = process.env.HOME;
|
||||
const origUserProfile = process.env.USERPROFILE;
|
||||
const origDir = process.env.CLAUDE_DIR;
|
||||
try {
|
||||
// Set up isolated environment
|
||||
@@ -2336,6 +2372,7 @@ file.ts
|
||||
const sessionsDir = path.join(claudeDir, 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
process.env.HOME = tmpDir;
|
||||
process.env.USERPROFILE = tmpDir; // Windows: os.homedir() uses USERPROFILE
|
||||
delete process.env.CLAUDE_DIR;
|
||||
|
||||
// Clear require cache for fresh module with new HOME
|
||||
@@ -2361,6 +2398,8 @@ file.ts
|
||||
'Non-matching date should return null');
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
if (origUserProfile !== undefined) process.env.USERPROFILE = origUserProfile;
|
||||
else delete process.env.USERPROFILE;
|
||||
if (origDir) process.env.CLAUDE_DIR = origDir;
|
||||
delete require.cache[require.resolve('../../scripts/lib/utils')];
|
||||
delete require.cache[require.resolve('../../scripts/lib/session-manager')];
|
||||
@@ -2450,6 +2489,7 @@ file.ts
|
||||
// "2026/01/15" or "Jan 15 2026" will never match, silently returning empty.
|
||||
// No validation or normalization occurs on the date parameter.
|
||||
const origHome = process.env.HOME;
|
||||
const origUserProfile = process.env.USERPROFILE;
|
||||
const origDir = process.env.CLAUDE_DIR;
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'r124-date-format-'));
|
||||
const homeDir = path.join(tmpDir, 'home');
|
||||
@@ -2457,6 +2497,7 @@ file.ts
|
||||
|
||||
try {
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir; // Windows: os.homedir() uses USERPROFILE
|
||||
delete process.env.CLAUDE_DIR;
|
||||
delete require.cache[require.resolve('../../scripts/lib/utils')];
|
||||
delete require.cache[require.resolve('../../scripts/lib/session-manager')];
|
||||
@@ -2495,6 +2536,8 @@ file.ts
|
||||
'null date skips filter and returns all sessions');
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
if (origUserProfile !== undefined) process.env.USERPROFILE = origUserProfile;
|
||||
else delete process.env.USERPROFILE;
|
||||
if (origDir) process.env.CLAUDE_DIR = origDir;
|
||||
delete require.cache[require.resolve('../../scripts/lib/utils')];
|
||||
delete require.cache[require.resolve('../../scripts/lib/session-manager')];
|
||||
|
||||
@@ -836,7 +836,8 @@ function runTests() {
|
||||
console.log('\nrunCommand Edge Cases:');
|
||||
|
||||
if (test('runCommand returns trimmed output', () => {
|
||||
const result = utils.runCommand('echo " hello "');
|
||||
// Windows echo includes quotes in output, use node to ensure consistent behavior
|
||||
const result = utils.runCommand('node -e "process.stdout.write(\' hello \')"');
|
||||
assert.strictEqual(result.success, true);
|
||||
assert.strictEqual(result.output, 'hello', 'Should trim leading/trailing whitespace');
|
||||
})) passed++; else failed++;
|
||||
@@ -884,6 +885,10 @@ function runTests() {
|
||||
console.log('\nreadStdinJson maxSize truncation:');
|
||||
|
||||
if (test('readStdinJson maxSize stops accumulating after threshold (chunk-level guard)', () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' (skipped — stdin chunking behavior differs on Windows)');
|
||||
return true;
|
||||
}
|
||||
const { execFileSync } = require('child_process');
|
||||
// maxSize is a chunk-level guard: once data.length >= maxSize, no MORE chunks are added.
|
||||
// A single small chunk that arrives when data.length < maxSize is added in full.
|
||||
@@ -1678,6 +1683,10 @@ function runTests() {
|
||||
// ── Round 110: findFiles root directory unreadable — silent empty return (not throw) ──
|
||||
console.log('\nRound 110: findFiles (root directory unreadable — EACCES on readdirSync caught silently):');
|
||||
if (test('findFiles returns empty array when root directory exists but is unreadable', () => {
|
||||
if (process.platform === 'win32' || process.getuid?.() === 0) {
|
||||
console.log(' (skipped — chmod ineffective on Windows/root)');
|
||||
return true;
|
||||
}
|
||||
const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r110-unreadable-root-'));
|
||||
const unreadableDir = path.join(tmpDir, 'no-read');
|
||||
fs.mkdirSync(unreadableDir);
|
||||
@@ -1697,7 +1706,7 @@ function runTests() {
|
||||
'Recursive search on unreadable root should also return empty array');
|
||||
} finally {
|
||||
// Restore permissions before cleanup
|
||||
try { fs.chmodSync(unreadableDir, 0o755); } catch {}
|
||||
try { fs.chmodSync(unreadableDir, 0o755); } catch (_e) { /* ignore permission errors */ }
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
Reference in New Issue
Block a user