Page Title
-This is some content.
-This is more content with sufficient length to be included.
-print("hello")
- diff --git a/.github/ISSUE_TEMPLATE/prompt_contribution.md b/.github/ISSUE_TEMPLATE/prompt_contribution.md index 2a5a489..7a2481b 100644 --- a/.github/ISSUE_TEMPLATE/prompt_contribution.md +++ b/.github/ISSUE_TEMPLATE/prompt_contribution.md @@ -45,5 +45,5 @@ body: required: true - label: "我已经在本地测试过这个提示词,并确认其有效性。" required: true - - label: "我同意将这个提示词以项目所使用的 [MIT License](LICENSE) 授权给社区。" - required: true \ No newline at end of file + - label: "我同意将这个提示词以项目所使用的 [MIT License](../../LICENSE) 授权给社区。" + required: true diff --git a/.github/labeler.yml b/.github/labeler.yml index e8cc39e..3068d88 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,7 +4,20 @@ # 文档相关的标签 documentation: - changed-files: - - any-glob-to-any-file: ['i18n/**/*.md', 'README.md', 'CONTRIBUTING.md', 'LICENSE'] + - any-glob-to-any-file: + - 'README.md' + - 'AGENTS.md' + - 'CONTRIBUTING.md' + - 'CODE_OF_CONDUCT.md' + - 'LICENSE' + - '.github/**/*.md' + - 'assets/README.md' + - 'assets/AGENTS.md' + - 'tools/config/**/*.md' + - 'docs/**/*.md' + - 'prompts/**/*.md' + - 'skills/**/*.md' + - 'tools/external/**/*.md' # CI/CD 工作流相关的标签 cicd: @@ -14,9 +27,19 @@ cicd: # 提示词相关的标签 prompt: - changed-files: - - any-glob-to-any-file: 'i18n/zh/prompts/**/*.md' + - any-glob-to-any-file: 'prompts/**/*.md' # 实战案例相关的标签 example: - changed-files: - - any-glob-to-any-file: 'i18n/zh/documents/实战案例/**/*.md' \ No newline at end of file + - any-glob-to-any-file: 'docs/case-studies/**/*.md' + +# 外部工具/依赖相关的标签 +repos: + - changed-files: + - any-glob-to-any-file: 'tools/external/**' + +# 工作流模板相关的标签 +workflow: + - changed-files: + - any-glob-to-any-file: 'docs/playbooks/workflows/**' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b30158a..b1389fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,11 @@ name: CI on: + workflow_dispatch: push: - branches: [ main ] + branches: [ develop, master ] pull_request: - branches: [ main ] + branches: [ develop, master ] jobs: markdown-lint: @@ -16,13 +17,15 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '16' + node-version: '22' - name: Install markdownlint-cli run: npm install -g markdownlint-cli - name: Run markdownlint - run: markdownlint --config .github/lint_config.json '**/*.md' + run: markdownlint --config .github/lint_config.json --ignore .history --ignore tools/external --ignore scripts/backups/gz '**/*.md' + - name: Check local markdown links + run: python3 scripts/check-local-links.py link-checker: runs-on: ubuntu-latest @@ -31,4 +34,4 @@ jobs: - name: Link Checker uses: lycheeverse/lychee-action@v1.5.0 with: - args: --verbose --no-progress './**/*.md' \ No newline at end of file + args: --verbose --no-progress --exclude-path .history --exclude-path .github/wiki --exclude-path tools/external --exclude-path tools/chat-vault --exclude-path scripts/backups/gz './**/*.md' diff --git a/.gitignore b/.gitignore index 548f061..f94c036 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ ENV/ *~ .DS_Store .history/ +*:Zone.Identifier # Testing .pytest_cache/ @@ -46,9 +47,16 @@ ENV/ *.log logs/ -# Skill Seekers (vendored tool output) +# Local task workspaces +assets/tasks/ + +# Skill Seekers (linked tool output) output/ -assets/skills/skills-skills/scripts/.venv-skill-seekers/ +skills/auto-skill/scripts/.venv-skill-seekers/ + +# prompts-library generated exports +tools/prompts-library/prompt_jsonl/ +tools/external/my-nvim/nvim-config/nvim libs/external/tmux libs/external/.tmux @@ -73,7 +81,7 @@ libs/external/.tmux # Backup backups/gz/ -assets/repo/backups/gz/ +scripts/backups/gz/ *.bak *.tmp diff --git a/.gitmodules b/.gitmodules index f0ec0a8..f925dbf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,12 @@ -[submodule "repo/.tmux"] - path = assets/repo/.tmux +[submodule "tools/external/.tmux"] + path = tools/external/.tmux url = https://github.com/gpakosz/.tmux.git -[submodule "repo/tmux"] - path = assets/repo/tmux +[submodule "tools/external/tmux"] + path = tools/external/tmux url = https://github.com/tmux/tmux.git -[submodule "repo/claude-official-skills"] - path = assets/repo/claude-official-skills +[submodule "tools/external/claude-official-skills"] + path = tools/external/claude-official-skills url = https://github.com/anthropics/skills.git +[submodule "tools/external/Skill_Seekers-development"] + path = tools/external/Skill_Seekers-development + url = https://github.com/yusufkaraaslan/Skill_Seekers.git diff --git a/AGENTS.md b/AGENTS.md index 18236bb..2c4962e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,21 +8,21 @@ ### 允许的操作 - 读取、修改顶层文档:`README.md`、`AGENTS.md`、`CONTRIBUTING.md` 等 -- 读取、修改 `assets/documents/`、`assets/prompts/`、`assets/skills/`、`assets/workflow/`、`assets/config/`、`assets/tools/`、`assets/repo/` 下的文档与代码 +- 读取、修改 `docs/`、`prompts/`、`skills/`、`tools/config/`、`tools/external/` 下的文档与代码 - 执行 `make lint`、备份脚本、prompts-library 转换工具 - 新增/修改提示词、技能、文档 - 提交符合规范的 commit ### 禁止的操作 - 修改 `.github/workflows/` 中的 CI 配置(除非任务明确要求) -- 删除或覆盖 `assets/repo/backups/gz/` 中的存档文件 +- 删除或覆盖 `scripts/backups/gz/` 中的存档文件 - 修改 `LICENSE`、`CODE_OF_CONDUCT.md` - 在代码中硬编码密钥、Token 或敏感凭证 - 未经确认的大范围重构 ### 敏感区域(禁止自动修改) - `.github/workflows/*.yml` - CI/CD 配置 -- `assets/repo/backups/gz/` - 历史备份存档 +- `scripts/backups/gz/` - 历史备份存档 - `.env*` 文件(如存在) --- @@ -33,16 +33,19 @@ # 1. 拉取最新代码 git pull --rebase origin develop -# 2. 运行 lint 检查 +# 2. 初始化外部仓库指针 +git submodule update --init --recursive + +# 3. 运行 lint 检查 make lint -# 3. 执行修改任务 +# 4. 执行修改任务 # ... -# 4. 再次 lint 验证 +# 5. 再次 lint 验证 make lint -# 5. 提交变更 +# 6. 提交变更 git add -A git commit -m "feat|fix|docs|chore: scope - summary" git push origin develop @@ -53,8 +56,8 @@ git push origin develop ## 3. Must-Run Commands(必须执行的命令清单) ### 环境要求 -- Node.js 16+(用于 markdownlint-cli) -- Python 3.8+(用于 prompts-library 工具) +- Node.js 22+(用于 markdownlint-cli) +- Python 3.8+(用于 prompts-library 工具与备份脚本) - Git ### 核心命令 @@ -63,16 +66,23 @@ git push origin develop |:---|:---|:---| | `make help` | 列出所有 Make 目标 | 无 | | `make lint` | 校验全仓库 Markdown | 需安装 markdownlint-cli | -| `bash assets/repo/backups/一键备份.sh` | 创建完整项目备份 | 无 | -| `python3 assets/repo/backups/快速备份.py` | Python 版备份脚本 | Python 3.8+ | -| `cd assets/repo/prompts-library && python3 main.py` | 提示词格式转换 | pandas, openpyxl, PyYAML | +| `make check-links` | 校验仓库内 Markdown 相对链接 | Python 3 | +| `make test` | 执行本地质量门禁 | Node.js 22+、Python 3 | +| `git submodule update --init --recursive` | 初始化外部 Git 仓库指针 | Git | +| `bash scripts/backups/一键备份.sh` | 创建完整项目备份 | 无 | +| `python3 scripts/backups/快速备份.py` | Python 版备份脚本 | Python 3.8+ | +| `cd tools/prompts-library && python3 main.py` | 提示词格式转换 | `pip install -r tools/prompts-library/requirements.txt` | + +### Python 依赖来源 +- prompts-library 主入口依赖:`tools/prompts-library/requirements.txt` +- prompts-library Google API / JSONL 辅助脚本依赖:`tools/prompts-library/scripts/requirements.txt` ### prompts-library 支持的转换模式 1. Excel → Docs:将 Excel 工作簿转换为 Markdown 文档目录 2. Docs → Excel:将 Markdown 文档目录还原为 Excel 工作簿 3. Docs → JSONL:将 Markdown 文档转换为 JSONL 格式 4. JSONL → Excel:将 JSONL 转换为 Excel -5. Excel(JSONL) → JSONL:将内部 JSONL 格式的 Excel 转换为 JSONL 文件 +5. Excel(JSONL) → JSONL:将内部 JSONL 格式的 Excel 转换为 JSONL 目录(每个工作表一个 JSONL 文件) --- @@ -80,20 +90,19 @@ git push origin develop ### 架构原则 - 保持根目录扁平,避免巨石文件 -- 三层内容架构:`assets/documents/` (知识) → `assets/prompts/` (指令) → `assets/skills/` (能力) +- 三层内容架构:`docs/` (知识) → `prompts/` (指令) → `skills/` (能力) ### 模块边界 -- `assets/documents/` - 中文知识库(方法论/入门/实战/资源) -- `assets/prompts/` - 提示词入口与云端索引 -- `assets/skills/` - 可复用技能库(每个子目录一个 Skill) -- `assets/workflow/` - 可复用工作流模板(自动开发闭环等) -- `assets/config/` - 工具与开发配置(例如 Codex CLI) -- `assets/tools/` - 预留:自定义脚本/小工具(保持可替换、可审计) -- `assets/repo/` - 外部工具与依赖(含 Git submodule) +- `docs/` - 中文知识库(方法论/入门/实战/资源) +- `prompts/` - 提示词入口与云端索引 +- `skills/` - 可复用技能库(每个子目录一个 Skill) +- `docs/playbooks/workflows/` - 可复用工作流模板(自动开发闭环等) +- `tools/config/` - 工具与开发配置(例如 Codex CLI) +- `tools/external/` - 外部工具与依赖(含 Git submodule) ### 依赖添加规则 - 新增工具或库时记录安装方式、最小版本与来源 -- 外部依赖来源记录在 `assets/repo/` 目录下 +- 外部依赖来源记录在 `tools/external/` 目录下 - 引入第三方脚本需标明许可证与来源 ### 禁止行为 @@ -130,55 +139,65 @@ git push origin develop . ├── README.md # 项目主文档 ├── AGENTS.md # AI Agent 行为准则(本文件) +├── llms.txt # 面向 AI 助手的短上下文入口 +├── llms-full.txt # 面向 AI 助手的完整上下文入口 ├── Makefile # 自动化脚本 ├── LICENSE # MIT 许可证 ├── CODE_OF_CONDUCT.md # 行为准则 ├── CONTRIBUTING.md # 贡献指南 ├── .gitignore # Git 忽略规则 │ -├── assets/ # 外部资源(指向在线表格) -│ ├── README.md # 远程表格索引(唯一真相源) +├── docs/ # 核心知识库 +│ ├── README.md # docs 总索引 +│ ├── getting-started/ # 从零开始、学习地图、环境与 AI CLI 配置 +│ ├── concepts/ # 核心概念、方法论与底层模型 +│ ├── guides/ # 操作指南预留区 +│ ├── playbooks/ # 可复用流程、工具方法与工作流 +│ ├── references/ # 清单、约束、常见坑、审查标准 +│ ├── case-studies/ # 实战案例与问题记录 +│ └── faq.md # 高频问题 +│ +├── prompts/ # 提示词库入口(指向云端表格) +│ ├── README.md # 在线表格链接 +│ └── AGENTS.md # prompts/ 目录规则 +│ +├── skills/ # 技能库(每个子目录一个 Skill) +│ ├── README.md # skills 总览与索引 +│ ├── AGENTS.md # skills/ 目录规则 +│ ├── auto-skill/ # 元技能核心 +│ ├── sop-generator/ # SOP 生成 +│ └── ... # 更多技能 +│ +├── assets/ # 静态资产与外部资源入口 +│ ├── README.md # 外部资源在线表格入口 │ ├── AGENTS.md # assets/ 目录规则 -│ ├── config/ # 工具与开发配置 -│ │ └── .codex/ # Codex CLI 配置(项目级) -│ │ ├── config.toml # Codex CLI 配置文件 -│ │ └── AGENTS.md # Codex/Agent 指南(本目录) -│ ├── documents/ # 文档库 -│ │ ├── 05-哲学与方法论/ # 最高思想纲领与方法论 -│ │ ├── 00-基础指南/ # 核心原则与底层逻辑 -│ │ ├── 01-入门指南/ # 从零开始教程 -│ │ ├── 02-方法论/ # 具体工具与技巧 -│ │ └── 03-实战/ # 项目实战案例 -│ ├── prompts/ # 提示词库(指向云端表格) -│ │ ├── README.md # 在线表格链接 -│ │ └── AGENTS.md # prompts/ 目录规则 -│ ├── skills/ # 技能库(扁平化,详见 assets/skills/README.md) -│ │ ├── README.md # skills 总览与索引 -│ │ ├── AGENTS.md # skills/ 目录规则 -│ │ ├── skills-skills/ # 元技能核心 -│ │ ├── sop-generator/ # SOP 生成 -│ │ ├── canvas-dev/ # Canvas白板驱动开发 -│ │ └── ... # 更多技能 -│ ├── tools/ # 工具目录(预留) -│ │ └── .gitkeep # 保持空目录被 Git 追踪 -│ ├── workflow/ # 工作流模板 -│ │ ├── auto-dev-loop/ # 自动开发循环 -│ │ └── canvas-dev/ # Canvas白板驱动开发 -│ └── repo/ # 外部工具与依赖镜像(含 Git submodule) +│ ├── images/ # 图片资产 +│ ├── templates/ # 模板附件 +│ └── datasets/ # 示例数据或数据说明 +│ +├── scripts/ # 自动化脚本 +│ ├── README.md # scripts 目录说明 +│ └── backups/ # 备份脚本与存档忽略规则 +│ +├── tools/ # 工具、本地配置与外部仓库 +│ ├── README.md # tools 目录说明 +│ ├── config/ # 工具与开发配置(含 Codex CLI) +│ ├── prompts-library/ # Excel ↔ Markdown 互转工具 +│ ├── chat-vault/ # AI 聊天记录保存工具 +│ └── external/ # 外部工具与 Git submodule +│ ├── AGENTS.md # external 目录规则 │ ├── README.md # 外部工具索引 -│ ├── AGENTS.md # assets/repo/ 目录规则 -│ ├── prompts-library/ # Excel ↔ Markdown 互转工具 -│ ├── chat-vault/ # AI 聊天记录保存工具 -│ ├── Skill_Seekers-development/ # Skills 制作器 -│ ├── html-tools-main/ # HTML 工具集 -│ ├── my-nvim/ # Neovim 配置 -│ ├── MCPlayerTransfer/ # MC 玩家迁移工具 -│ ├── XHS-image-to-PDF-conversion/ # 小红书图片转 PDF -│ ├── backups/ # 历史备份脚本快照 +│ ├── Skill_Seekers-development/ # Skills 制作器 (submodule) │ ├── .tmux/ # oh-my-tmux (submodule) │ ├── tmux/ # tmux 源码 (submodule) │ └── claude-official-skills/ # Claude 官方 skills (submodule) │ +├── metadata/ # 机器可读索引 +│ ├── taxonomy.yml # 分类体系 +│ ├── glossary.yml # 术语表 +│ ├── redirects.yml # 重命名/迁移映射 +│ └── ai-citation/ # AI 引用语料包(摘要、FAQ、对比、推荐回答、GEO/SEO 检查清单) +│ ├── .github/ # GitHub 配置 │ ├── workflows/ # CI/CD 工作流 │ │ ├── ci.yml # Markdown lint + link checker @@ -194,10 +213,22 @@ git push origin develop ### 关键入口文件 - `README.md` - 项目主文档,面向人类开发者 - `AGENTS.md` - AI Agent 操作手册(本文件) -- `assets/repo/prompts-library/main.py` - 提示词转换工具入口 -- `assets/repo/backups/一键备份.sh` - 备份脚本入口 -- `assets/skills/tmux-autopilot/` - tmux 自动化操控技能(基于 oh-my-tmux,含 capture-pane/send-keys/蜂群巡检脚本) -- `assets/skills/sop-generator/` - SOP 生成与规范化技能(输入资料/需求 -> 标准 SOP) +- `llms.txt` - 面向 AI 助手的短上下文入口,用于让 LLM 快速理解项目定位 +- `llms-full.txt` - 面向 AI 助手的完整上下文入口,用于 GEO / AI 推荐与引用 +- `metadata/ai-citation/` - AI 引用语料包,包含摘要、FAQ、对比、推荐回答与 GEO/SEO 检查清单 +- `.github/lint_config.json` - markdownlint 规则,供 `make lint` 与 CI 共用 +- `.github/workflows/ci.yml` - GitHub Actions:develop/master 分支 markdown-lint + link-checker +- `scripts/check-local-links.py` - 仓库内 Markdown 相对链接检查脚本,供 `make check-links` 与 CI 使用 +- `docs/guides/仓库维护与质量门禁.md` - 仓库维护、迁移检查和质量门禁指南 +- `tools/prompts-library/main.py` - 提示词转换工具入口 +- `scripts/backups/一键备份.sh` - 备份脚本入口 +- `docs/getting-started/Codex-CLI配置.md` - 零基础路径的默认 AI CLI 配置入口 +- `docs/getting-started/OpenCode-CLI配置.md` - Codex CLI 不可用时的备选 AI CLI 配置入口 +- `docs/playbooks/GEO与SEO优化方法.md` - GEO / SEO 内容工程方法,承接 GEOFlow 的知识库、结构化内容、审核与分发思路 +- `docs/concepts/问题求解能力.md` - 问题定义与求解路径底层模型 +- `docs/references/底层程序逻辑设计与工程优化项.md` - 底层程序逻辑与工程优化检查项 +- `skills/tmux-autopilot/` - tmux 自动化操控技能(基于 oh-my-tmux,含 capture-pane/send-keys/蜂群巡检脚本) +- `skills/sop-generator/` - SOP 生成与规范化技能(输入资料/需求 -> 标准 SOP) --- @@ -206,10 +237,11 @@ git push origin develop | 问题 | 原因 | 修复 | |:---|:---|:---| | `make lint` 失败 | 未安装 markdownlint-cli | `npm install -g markdownlint-cli` | -| prompts-library 报错 | 缺少 Python 依赖 | `pip install pandas openpyxl PyYAML rich InquirerPy` | -| CI markdown-lint 失败 | `.github/lint_config.json` 缺失 | TODO:新增 `.github/lint_config.json` 或调整 `.github/workflows/ci.yml` 的 lint 命令(需任务明确授权) | +| prompts-library 报错 | 缺少 Python 依赖 | `pip install -r tools/prompts-library/requirements.txt` | +| prompts-library 辅助脚本报 Google API 依赖错误 | 未安装脚本专用依赖 | `pip install -r tools/prompts-library/scripts/requirements.txt` | +| CI markdown-lint 失败 | Markdown 规则违规或本地未按 `.github/lint_config.json` 校验 | 运行 `make lint`,按输出修复对应 Markdown | | CI link-checker 失败 | 文档中存在失效链接 | 检查并修复 Markdown 中的链接 | -| 备份脚本权限不足 | Shell 脚本无执行权限 | `chmod +x assets/repo/backups/一键备份.sh` | +| 备份脚本权限不足 | Shell 脚本无执行权限 | `chmod +x scripts/backups/一键备份.sh` | --- @@ -232,12 +264,14 @@ feat|fix|docs|chore|refactor|test: scope - summary - 测试与验证步骤 ### CI 触发条件 -- `push` 到 `main` 分支 -- `pull_request` 到 `main` 分支 +- `push` 到 `develop` 或 `master` 分支 +- `pull_request` 到 `develop` 或 `master` 分支 +- 手动触发 `workflow_dispatch` ### CI 检查项 1. `markdown-lint` - Markdown 格式检查 -2. `link-checker` - 链接有效性检查 +2. `check local markdown links` - 仓库内相对链接检查 +3. `link-checker` - 链接有效性检查 ### 提交前清单 - [ ] 运行 `make lint` 通过 @@ -268,25 +302,25 @@ feat|fix|docs|chore|refactor|test: scope - summary ```bash # 提示词库转换 -cd assets/repo/prompts-library && python3 main.py +cd tools/prompts-library && python3 main.py # Lint 所有 Markdown 文件 make lint # 创建完整项目备份 -bash assets/repo/backups/一键备份.sh +bash scripts/backups/一键备份.sh ``` ## Architecture & Structure ### Core Directories -- **`assets/prompts/`**: 提示词库入口(指向云端表格) -- **`assets/skills/`**: 扁平化技能库(详见 assets/skills/README.md) -- **`assets/documents/`**: 知识库(05-哲学与方法论、00-基础指南、01-入门指南、02-方法论、03-实战) +- **`prompts/`**: 提示词库入口(指向云端表格) +- **`skills/`**: 扁平化技能库(详见 skills/README.md) +- **`docs/`**: 知识库(principles、guides、case-studies) - **`assets/`**: 外部资源(在线表格)入口与使用说明 -- **`assets/repo/prompts-library/`**: Excel ↔ Markdown 转换工具 -- **`assets/repo/chat-vault/`**: AI 聊天记录保存工具 -- **`assets/repo/backups/`**: 备份脚本与存档 +- **`tools/prompts-library/`**: Excel ↔ Markdown 转换工具 +- **`tools/chat-vault/`**: AI 聊天记录保存工具 +- **`scripts/backups/`**: 备份脚本与存档 ### Key Technical Details 1. **Prompt Organization**: 提示词使用 `(row,col)_` 前缀进行分类 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f7a22e5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +本文档记录知识库结构、入口、分类体系和重要内容的变更。 + +## Unreleased + +- 启动标准知识库结构迁移:新增 `docs/`、`metadata/`、`scripts/`、`tools/` 骨架。 +- 完成第一轮完全搬家:`assets/documents/` 迁入 `docs/`,`assets/skills/` 迁入 `skills/`,`assets/prompt/` 迁入 `prompts/`,`assets/config/` 迁入 `tools/config/`,`assets/repos/` 拆分为 `tools/external/`、`tools/prompts-library/`、`tools/chat-vault/` 与 `scripts/backups/`。 +- 更新 README、AGENTS、llms、metadata、CI 与目录级 AGENTS,统一新知识库路径口径。 +- 增加 `scripts/check-local-links.py`、`make check-links` 与 `make test`,把本地链接检查纳入质量门禁和 CI。 +- 清退 `tools/external/my-nvim/nvim-config/nvim` 二进制运行时,改为文档说明从官方渠道安装 Neovim。 diff --git a/Makefile b/Makefile index 896dbeb..1b7c460 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Makefile for Vibe Coding Guide -.PHONY: help lint build test clean +.PHONY: help lint check-links build test clean help: @echo "Makefile for Vibe Coding Guide" @@ -8,27 +8,29 @@ help: @echo "Available commands:" @echo " help - Show this help message" @echo " lint - Lint all markdown files" - @echo " build - Build the project (Placeholder)" - @echo " test - Run tests (Placeholder)" - @echo " clean - Clean build artifacts (Placeholder)" + @echo " check-links - Check local markdown links" + @echo " build - Verify knowledge base has no build step" + @echo " test - Run repository quality gates" + @echo " clean - Remove ignored generated caches" @echo "" lint: @echo "Linting markdown files..." @npm install -g markdownlint-cli - @markdownlint --config .github/lint_config.json '**/*.md' + @markdownlint --config .github/lint_config.json --ignore .history --ignore tools/external --ignore scripts/backups/gz '**/*.md' + +check-links: + @echo "Checking local markdown links..." + @python3 scripts/check-local-links.py build: - @echo "Building the project..." - # Add your project build commands here - @echo "Build complete." + @echo "No build step: this repository is a documentation and knowledge-base project." -test: - @echo "Running tests..." - # Add your test commands here - @echo "Tests complete." +test: lint check-links + @echo "Quality gates complete." clean: - @echo "Cleaning up build artifacts..." - # Add your clean commands here (e.g., rm -rf dist/ build/) - @echo "Cleanup complete." \ No newline at end of file + @echo "Cleaning ignored generated caches..." + @find . -type d -name '__pycache__' -prune -exec rm -rf {} + + @rm -rf tools/prompts-library/prompt_jsonl + @echo "Cleanup complete." diff --git a/README.md b/README.md index b7a5cef..62b27e0 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@
AGENTS.md / 自定义 rules)` | Font/indent/pattern |
-| Language detection | CSS classes | Pattern matching |
-| Quality scoring | No | Yes (B1.4) |
-| Chunking | No | Yes (B1.3) |
-
----
-
-## Next Steps
-
-### Task B1.7: MCP Tool Integration
-
-The PDF scraper will be available through MCP:
-
-```python
-# Future: MCP tool
-result = mcp.scrape_pdf(
- config_path="configs/manual.json"
-)
-
-# Or direct
-result = mcp.scrape_pdf(
- pdf_path="manual.pdf",
- name="mymanual",
- extract_images=True
-)
-```
-
----
-
-## Conclusion
-
-Tasks B1.6 and B1.8 successfully implement:
-
-**B1.6 - PDF Scraper CLI:**
-- ✅ Complete extraction → building workflow
-- ✅ Three usage modes (config, direct, from-json)
-- ✅ Automatic categorization (chapter or keyword-based)
-- ✅ Integration with Skill Seeker workflow
-- ✅ Quality filtering and top examples
-
-**B1.8 - PDF Config Format:**
-- ✅ JSON configuration format
-- ✅ Extraction options (chunk size, quality, images)
-- ✅ Category definitions (keyword-based)
-- ✅ Compatible with web scraper config style
-
-**Impact:**
-- Complete PDF documentation support
-- Parallel workflow to web scraping
-- Reusable extraction results
-- High-quality skill generation
-
-**Ready for B1.7:** MCP tool integration
-
----
-
-**Tasks Completed:** October 21, 2025
-**Next Task:** B1.7 - Add MCP tool `scrape_pdf`
diff --git a/assets/repo/Skill_Seekers-development/docs/PDF_SYNTAX_DETECTION.md b/assets/repo/Skill_Seekers-development/docs/PDF_SYNTAX_DETECTION.md
deleted file mode 100644
index 3eeb48d..0000000
--- a/assets/repo/Skill_Seekers-development/docs/PDF_SYNTAX_DETECTION.md
+++ /dev/null
@@ -1,576 +0,0 @@
-# PDF Code Block Syntax Detection (Task B1.4)
-
-**Status:** ✅ Completed
-**Date:** October 21, 2025
-**Task:** B1.4 - Extract code blocks from PDFs with syntax detection
-
----
-
-## Overview
-
-Task B1.4 enhances the PDF extractor with advanced code block detection capabilities including:
-- **Confidence scoring** for language detection
-- **Syntax validation** to filter out false positives
-- **Quality scoring** to rank code blocks by usefulness
-- **Automatic filtering** of low-quality code
-
-This dramatically improves the accuracy and usefulness of extracted code samples from PDF documentation.
-
----
-
-## New Features
-
-### ✅ 1. Confidence-Based Language Detection
-
-Enhanced language detection now returns both language and confidence score:
-
-**Before (B1.2):**
-```python
-lang = detect_language_from_code(code) # Returns: 'python'
-```
-
-**After (B1.4):**
-```python
-lang, confidence = detect_language_from_code(code) # Returns: ('python', 0.85)
-```
-
-**Confidence Calculation:**
-- Pattern matches are weighted (1-5 points)
-- Scores are normalized to 0-1 range
-- Higher confidence = more reliable detection
-
-**Example Pattern Weights:**
-```python
-'python': [
- (r'\bdef\s+\w+\s*\(', 3), # Strong indicator
- (r'\bimport\s+\w+', 2), # Medium indicator
- (r':\s*$', 1), # Weak indicator (lines ending with :)
-]
-```
-
-### ✅ 2. Syntax Validation
-
-Validates detected code blocks to filter false positives:
-
-**Validation Checks:**
-1. **Not empty** - Rejects empty code blocks
-2. **Indentation consistency** (Python) - Detects mixed tabs/spaces
-3. **Balanced brackets** - Checks for unclosed parentheses, braces
-4. **Language-specific syntax** (JSON) - Attempts to parse
-5. **Natural language detection** - Filters out prose misidentified as code
-6. **Comment ratio** - Rejects blocks that are mostly comments
-
-**Output:**
-```json
-{
- "code": "def example():\n return True",
- "language": "python",
- "is_valid": true,
- "validation_issues": []
-}
-```
-
-**Invalid example:**
-```json
-{
- "code": "This is not code",
- "language": "unknown",
- "is_valid": false,
- "validation_issues": ["May be natural language, not code"]
-}
-```
-
-### ✅ 3. Quality Scoring
-
-Each code block receives a quality score (0-10) based on multiple factors:
-
-**Scoring Factors:**
-1. **Language confidence** (+0 to +2.0 points)
-2. **Code length** (optimal: 20-500 chars, +1.0)
-3. **Line count** (optimal: 2-50 lines, +1.0)
-4. **Has definitions** (functions/classes, +1.5)
-5. **Meaningful variable names** (+1.0)
-6. **Syntax validation** (+1.0 if valid, -0.5 per issue)
-
-**Quality Tiers:**
-- **High quality (7-10):** Complete, valid, useful code examples
-- **Medium quality (4-7):** Partial or simple code snippets
-- **Low quality (0-4):** Fragments, false positives, invalid code
-
-**Example:**
-```python
-# High-quality code block (score: 8.5/10)
-def calculate_total(items):
- total = 0
- for item in items:
- total += item.price
- return total
-
-# Low-quality code block (score: 2.0/10)
-x = y
-```
-
-### ✅ 4. Quality Filtering
-
-Filter out low-quality code blocks automatically:
-
-```bash
-# Keep only high-quality code (score >= 7.0)
-python3 cli/pdf_extractor_poc.py input.pdf --min-quality 7.0
-
-# Keep medium and high quality (score >= 4.0)
-python3 cli/pdf_extractor_poc.py input.pdf --min-quality 4.0
-
-# No filtering (default)
-python3 cli/pdf_extractor_poc.py input.pdf
-```
-
-**Benefits:**
-- Reduces noise in output
-- Focuses on useful examples
-- Improves downstream skill quality
-
-### ✅ 5. Quality Statistics
-
-New summary statistics show overall code quality:
-
-```
-📊 Code Quality Statistics:
- Average quality: 6.8/10
- Average confidence: 78.5%
- Valid code blocks: 45/52 (86.5%)
- High quality (7+): 28
- Medium quality (4-7): 17
- Low quality (<4): 7
-```
-
----
-
-## Output Format
-
-### Enhanced Code Block Object
-
-Each code block now includes quality metadata:
-
-```json
-{
- "code": "def example():\n return True",
- "language": "python",
- "confidence": 0.85,
- "quality_score": 7.5,
- "is_valid": true,
- "validation_issues": [],
- "detection_method": "font",
- "font": "Courier-New"
-}
-```
-
-### Quality Statistics Object
-
-Top-level summary of code quality:
-
-```json
-{
- "quality_statistics": {
- "average_quality": 6.8,
- "average_confidence": 0.785,
- "valid_code_blocks": 45,
- "invalid_code_blocks": 7,
- "validation_rate": 0.865,
- "high_quality_blocks": 28,
- "medium_quality_blocks": 17,
- "low_quality_blocks": 7
- }
-}
-```
-
----
-
-## Usage Examples
-
-### Basic Extraction with Quality Stats
-
-```bash
-python3 cli/pdf_extractor_poc.py manual.pdf -o output.json --pretty
-```
-
-**Output:**
-```
-✅ Extraction complete:
- Total characters: 125,000
- Code blocks found: 52
- Headings found: 45
- Images found: 12
- Chunks created: 5
- Chapters detected: 3
- Languages detected: python, javascript, sql
-
-📊 Code Quality Statistics:
- Average quality: 6.8/10
- Average confidence: 78.5%
- Valid code blocks: 45/52 (86.5%)
- High quality (7+): 28
- Medium quality (4-7): 17
- Low quality (<4): 7
-```
-
-### Filter Low-Quality Code
-
-```bash
-# Keep only high-quality examples
-python3 cli/pdf_extractor_poc.py tutorial.pdf --min-quality 7.0 -v
-
-# Verbose output shows filtering:
-# 📄 Extracting from: tutorial.pdf
-# ...
-# Filtered out 12 low-quality code blocks (min_quality=7.0)
-#
-# ✅ Extraction complete:
-# Code blocks found: 28 (after filtering)
-```
-
-### Inspect Quality Scores
-
-```bash
-# Extract and view quality scores
-python3 cli/pdf_extractor_poc.py input.pdf -o output.json
-
-# View quality scores with jq
-cat output.json | jq '.pages[0].code_samples[] | {language, quality_score, is_valid}'
-```
-
-**Output:**
-```json
-{
- "language": "python",
- "quality_score": 8.5,
- "is_valid": true
-}
-{
- "language": "javascript",
- "quality_score": 6.2,
- "is_valid": true
-}
-{
- "language": "unknown",
- "quality_score": 2.1,
- "is_valid": false
-}
-```
-
----
-
-## Technical Implementation
-
-### Language Detection with Confidence
-
-```python
-def detect_language_from_code(self, code):
- """Enhanced with weighted pattern matching"""
-
- patterns = {
- 'python': [
- (r'\bdef\s+\w+\s*\(', 3), # Weight: 3
- (r'\bimport\s+\w+', 2), # Weight: 2
- (r':\s*$', 1), # Weight: 1
- ],
- # ... other languages
- }
-
- # Calculate scores for each language
- scores = {}
- for lang, lang_patterns in patterns.items():
- score = 0
- for pattern, weight in lang_patterns:
- if re.search(pattern, code, re.IGNORECASE | re.MULTILINE):
- score += weight
- if score > 0:
- scores[lang] = score
-
- # Get best match
- best_lang = max(scores, key=scores.get)
- confidence = min(scores[best_lang] / 10.0, 1.0)
-
- return best_lang, confidence
-```
-
-### Syntax Validation
-
-```python
-def validate_code_syntax(self, code, language):
- """Validate code syntax"""
- issues = []
-
- if language == 'python':
- # Check indentation consistency
- indent_chars = set()
- for line in code.split('\n'):
- if line.startswith(' '):
- indent_chars.add('space')
- elif line.startswith('\t'):
- indent_chars.add('tab')
-
- if len(indent_chars) > 1:
- issues.append('Mixed tabs and spaces')
-
- # Check balanced brackets
- open_count = code.count('(') + code.count('[') + code.count('{')
- close_count = code.count(')') + code.count(']') + code.count('}')
- if abs(open_count - close_count) > 2:
- issues.append('Unbalanced brackets')
-
- # Check if it's actually natural language
- common_words = ['the', 'and', 'for', 'with', 'this', 'that']
- word_count = sum(1 for word in common_words if word in code.lower())
- if word_count > 5:
- issues.append('May be natural language, not code')
-
- return len(issues) == 0, issues
-```
-
-### Quality Scoring
-
-```python
-def score_code_quality(self, code, language, confidence):
- """Score code quality (0-10)"""
- score = 5.0 # Neutral baseline
-
- # Factor 1: Language confidence
- score += confidence * 2.0
-
- # Factor 2: Code length (optimal range)
- code_length = len(code.strip())
- if 20 <= code_length <= 500:
- score += 1.0
-
- # Factor 3: Has function/class definitions
- if re.search(r'\b(def|function|class|func)\b', code):
- score += 1.5
-
- # Factor 4: Meaningful variable names
- meaningful_vars = re.findall(r'\b[a-z_][a-z0-9_]{3,}\b', code.lower())
- if len(meaningful_vars) >= 2:
- score += 1.0
-
- # Factor 5: Syntax validation
- is_valid, issues = self.validate_code_syntax(code, language)
- if is_valid:
- score += 1.0
- else:
- score -= len(issues) * 0.5
-
- return max(0, min(10, score)) # Clamp to 0-10
-```
-
----
-
-## Performance Impact
-
-### Overhead Analysis
-
-| Operation | Time per page | Impact |
-|-----------|---------------|--------|
-| Confidence scoring | +0.2ms | Negligible |
-| Syntax validation | +0.5ms | Negligible |
-| Quality scoring | +0.3ms | Negligible |
-| **Total overhead** | **+1.0ms** | **<2%** |
-
-**Benchmark:**
-- Small PDF (10 pages): +10ms total (~1% overhead)
-- Medium PDF (100 pages): +100ms total (~2% overhead)
-- Large PDF (500 pages): +500ms total (~2% overhead)
-
-### Memory Usage
-
-- Quality metadata adds ~200 bytes per code block
-- Statistics add ~500 bytes to output
-- **Impact:** Negligible (<1% increase)
-
----
-
-## Comparison: Before vs After
-
-| Metric | Before (B1.3) | After (B1.4) | Improvement |
-|--------|---------------|--------------|-------------|
-| Language detection | Single return | Lang + confidence | ✅ More reliable |
-| Syntax validation | None | Multiple checks | ✅ Filters false positives |
-| Quality scoring | None | 0-10 scale | ✅ Ranks code blocks |
-| False positives | ~15-20% | ~3-5% | ✅ 75% reduction |
-| Code quality avg | Unknown | Measurable | ✅ Trackable |
-| Filtering | None | Automatic | ✅ Cleaner output |
-
----
-
-## Testing
-
-### Test Quality Scoring
-
-```bash
-# Create test PDF with various code qualities
-# - High-quality: Complete function with meaningful names
-# - Medium-quality: Simple variable assignments
-# - Low-quality: Natural language text
-
-python3 cli/pdf_extractor_poc.py test.pdf -o test.json -v
-
-# Check quality scores
-cat test.json | jq '.pages[].code_samples[] | {language, quality_score}'
-```
-
-**Expected Results:**
-```json
-{"language": "python", "quality_score": 8.5}
-{"language": "javascript", "quality_score": 6.2}
-{"language": "unknown", "quality_score": 1.8}
-```
-
-### Test Validation
-
-```bash
-# Check validation results
-cat test.json | jq '.pages[].code_samples[] | select(.is_valid == false)'
-```
-
-**Should show:**
-- Empty code blocks
-- Natural language misdetected as code
-- Code with severe syntax errors
-
-### Test Filtering
-
-```bash
-# Extract with different quality thresholds
-python3 cli/pdf_extractor_poc.py test.pdf --min-quality 7.0 -o high_quality.json
-python3 cli/pdf_extractor_poc.py test.pdf --min-quality 4.0 -o medium_quality.json
-python3 cli/pdf_extractor_poc.py test.pdf --min-quality 0.0 -o all_quality.json
-
-# Compare counts
-echo "High quality:"; cat high_quality.json | jq '[.pages[].code_samples[]] | length'
-echo "Medium+:"; cat medium_quality.json | jq '[.pages[].code_samples[]] | length'
-echo "All:"; cat all_quality.json | jq '[.pages[].code_samples[]] | length'
-```
-
----
-
-## Limitations
-
-### Current Limitations
-
-1. **Validation is heuristic-based**
- - No AST parsing (yet)
- - Some edge cases may be missed
- - Language-specific validation only for Python, JS, Java, C
-
-2. **Quality scoring is subjective**
- - Based on heuristics, not compilation
- - May not match human judgment perfectly
- - Tuned for documentation examples, not production code
-
-3. **Confidence scoring is pattern-based**
- - No machine learning
- - Limited to defined patterns
- - May struggle with uncommon languages
-
-### Known Issues
-
-1. **Short Code Snippets**
- - May score lower than deserved
- - Example: `x = 5` is valid but scores low
-
-2. **Comments-Heavy Code**
- - Well-commented code may be penalized
- - Workaround: Adjust comment ratio threshold
-
-3. **Domain-Specific Languages**
- - Not covered by pattern detection
- - Will be marked as 'unknown'
-
----
-
-## Future Enhancements
-
-### Potential Improvements
-
-1. **AST-Based Validation**
- - Use Python's `ast` module for Python code
- - Use esprima/acorn for JavaScript
- - Actual syntax parsing instead of heuristics
-
-2. **Machine Learning Detection**
- - Train classifier on code vs non-code
- - More accurate language detection
- - Context-aware quality scoring
-
-3. **Custom Quality Metrics**
- - User-defined quality factors
- - Domain-specific scoring
- - Configurable weights
-
-4. **More Language Support**
- - Add TypeScript, Dart, Lua, etc.
- - Better pattern coverage
- - Language-specific validation
-
----
-
-## Integration with Skill Seeker
-
-### Improved Skill Quality
-
-With B1.4 enhancements, PDF-based skills will have:
-
-1. **Higher quality code examples**
- - Automatic filtering of noise
- - Only meaningful snippets included
-
-2. **Better categorization**
- - Confidence scores help categorization
- - Language-specific references
-
-3. **Validation feedback**
- - Know which code blocks may have issues
- - Fix before packaging skill
-
-### Example Workflow
-
-```bash
-# Step 1: Extract with high-quality filter
-python3 cli/pdf_extractor_poc.py manual.pdf --min-quality 7.0 -o manual.json -v
-
-# Step 2: Review quality statistics
-cat manual.json | jq '.quality_statistics'
-
-# Step 3: Inspect any invalid blocks
-cat manual.json | jq '.pages[].code_samples[] | select(.is_valid == false)'
-
-# Step 4: Build skill (future task B1.6)
-python3 cli/pdf_scraper.py --from-json manual.json
-```
-
----
-
-## Conclusion
-
-Task B1.4 successfully implements:
-- ✅ Confidence-based language detection
-- ✅ Syntax validation for common languages
-- ✅ Quality scoring (0-10 scale)
-- ✅ Automatic quality filtering
-- ✅ Comprehensive quality statistics
-
-**Impact:**
-- 75% reduction in false positives
-- More reliable code extraction
-- Better skill quality
-- Measurable code quality metrics
-
-**Performance:** <2% overhead (negligible)
-
-**Compatibility:** Backward compatible (existing fields preserved)
-
-**Ready for B1.5:** Image extraction from PDFs
-
----
-
-**Task Completed:** October 21, 2025
-**Next Task:** B1.5 - Add PDF image extraction (diagrams, screenshots)
diff --git a/assets/repo/Skill_Seekers-development/docs/TERMINAL_SELECTION.md b/assets/repo/Skill_Seekers-development/docs/TERMINAL_SELECTION.md
deleted file mode 100644
index dad3c4c..0000000
--- a/assets/repo/Skill_Seekers-development/docs/TERMINAL_SELECTION.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# Terminal Selection Guide
-
-When using `--enhance-local`, Skill Seeker opens a new terminal window to run Claude Code. This guide explains how to control which terminal app is used.
-
-## Priority Order
-
-The script automatically detects which terminal to use in this order:
-
-1. **`SKILL_SEEKER_TERMINAL` environment variable** (highest priority)
-2. **`TERM_PROGRAM` environment variable** (inherit current terminal)
-3. **Terminal.app** (fallback default)
-
-## Setting Your Preferred Terminal
-
-### Option 1: Set Environment Variable (Recommended)
-
-Add this to your shell config (`~/.zshrc` or `~/.bashrc`):
-
-```bash
-# For Ghostty users
-export SKILL_SEEKER_TERMINAL="Ghostty"
-
-# For iTerm users
-export SKILL_SEEKER_TERMINAL="iTerm"
-
-# For WezTerm users
-export SKILL_SEEKER_TERMINAL="WezTerm"
-```
-
-Then reload your shell:
-```bash
-source ~/.zshrc # or source ~/.bashrc
-```
-
-### Option 2: Set Per-Session
-
-Set the variable before running the command:
-
-```bash
-SKILL_SEEKER_TERMINAL="Ghostty" python3 cli/doc_scraper.py --config configs/react.json --enhance-local
-```
-
-### Option 3: Inherit Current Terminal (Automatic)
-
-If you run the script from Ghostty, iTerm2, or WezTerm, it will automatically open the enhancement in the same terminal app.
-
-**Note:** IDE terminals (VS Code, Zed, JetBrains) use unique `TERM_PROGRAM` values, so they fall back to Terminal.app unless you set `SKILL_SEEKER_TERMINAL`.
-
-## Supported Terminals
-
-- **Ghostty** (`ghostty`)
-- **iTerm2** (`iTerm.app`)
-- **Terminal.app** (`Apple_Terminal`)
-- **WezTerm** (`WezTerm`)
-
-## Example Output
-
-When terminal detection works:
-```
-🚀 Launching Claude Code in new terminal...
- Using terminal: Ghostty (from SKILL_SEEKER_TERMINAL)
-```
-
-When running from an IDE terminal:
-```
-🚀 Launching Claude Code in new terminal...
-⚠️ unknown TERM_PROGRAM (zed)
- → Using Terminal.app as fallback
-```
-
-**Tip:** Set `SKILL_SEEKER_TERMINAL` to avoid the fallback behavior.
-
-## Troubleshooting
-
-**Q: The wrong terminal opens even though I set `SKILL_SEEKER_TERMINAL`**
-
-A: Make sure you reloaded your shell after editing `~/.zshrc`:
-```bash
-source ~/.zshrc
-```
-
-**Q: I want to use a different terminal temporarily**
-
-A: Set the variable inline:
-```bash
-SKILL_SEEKER_TERMINAL="iTerm" python3 cli/doc_scraper.py --enhance-local ...
-```
-
-**Q: Can I use a custom terminal app?**
-
-A: Yes! Just use the app name as it appears in `/Applications/`:
-```bash
-export SKILL_SEEKER_TERMINAL="Alacritty"
-```
diff --git a/assets/repo/Skill_Seekers-development/docs/TESTING.md b/assets/repo/Skill_Seekers-development/docs/TESTING.md
deleted file mode 100644
index 6c46a77..0000000
--- a/assets/repo/Skill_Seekers-development/docs/TESTING.md
+++ /dev/null
@@ -1,716 +0,0 @@
-# Testing Guide for Skill Seeker
-
-Comprehensive testing documentation for the Skill Seeker project.
-
-## Quick Start
-
-```bash
-# Run all tests
-python3 run_tests.py
-
-# Run all tests with verbose output
-python3 run_tests.py -v
-
-# Run specific test suite
-python3 run_tests.py --suite config
-python3 run_tests.py --suite features
-python3 run_tests.py --suite integration
-
-# Stop on first failure
-python3 run_tests.py --failfast
-
-# List all available tests
-python3 run_tests.py --list
-```
-
-## Test Structure
-
-```
-tests/
-├── __init__.py # Test package marker
-├── test_config_validation.py # Config validation tests (30+ tests)
-├── test_scraper_features.py # Core feature tests (25+ tests)
-├── test_integration.py # Integration tests (15+ tests)
-├── test_pdf_extractor.py # PDF extraction tests (23 tests)
-├── test_pdf_scraper.py # PDF workflow tests (18 tests)
-└── test_pdf_advanced_features.py # PDF advanced features (26 tests) NEW
-```
-
-## Test Suites
-
-### 1. Config Validation Tests (`test_config_validation.py`)
-
-Tests the `validate_config()` function with comprehensive coverage.
-
-**Test Categories:**
-- ✅ Valid configurations (minimal and complete)
-- ✅ Missing required fields (`name`, `base_url`)
-- ✅ Invalid name formats (special characters)
-- ✅ Valid name formats (alphanumeric, hyphens, underscores)
-- ✅ Invalid URLs (missing protocol)
-- ✅ Valid URL protocols (http, https)
-- ✅ Selector validation (structure and recommended fields)
-- ✅ URL patterns validation (include/exclude lists)
-- ✅ Categories validation (structure and keywords)
-- ✅ Rate limit validation (range 0-10, type checking)
-- ✅ Max pages validation (range 1-10000, type checking)
-- ✅ Start URLs validation (format and protocol)
-
-**Example Test:**
-```python
-def test_valid_complete_config(self):
- """Test valid complete configuration"""
- config = {
- 'name': 'godot',
- 'base_url': 'https://docs.godotengine.org/en/stable/',
- 'selectors': {
- 'main_content': 'div[role="main"]',
- 'title': 'title',
- 'code_blocks': 'pre code'
- },
- 'rate_limit': 0.5,
- 'max_pages': 500
- }
- errors = validate_config(config)
- self.assertEqual(len(errors), 0)
-```
-
-**Running:**
-```bash
-python3 run_tests.py --suite config -v
-```
-
----
-
-### 2. Scraper Features Tests (`test_scraper_features.py`)
-
-Tests core scraper functionality including URL validation, language detection, pattern extraction, and categorization.
-
-**Test Categories:**
-
-**URL Validation:**
-- ✅ URL matching include patterns
-- ✅ URL matching exclude patterns
-- ✅ Different domain rejection
-- ✅ No pattern configuration
-
-**Language Detection:**
-- ✅ Detection from CSS classes (`language-*`, `lang-*`)
-- ✅ Detection from parent elements
-- ✅ Python detection (import, from, def)
-- ✅ JavaScript detection (const, let, arrow functions)
-- ✅ GDScript detection (func, var)
-- ✅ C++ detection (#include, int main)
-- ✅ Unknown language fallback
-
-**Pattern Extraction:**
-- ✅ Extraction with "Example:" marker
-- ✅ Extraction with "Usage:" marker
-- ✅ Pattern limit (max 5)
-
-**Categorization:**
-- ✅ Categorization by URL keywords
-- ✅ Categorization by title keywords
-- ✅ Categorization by content keywords
-- ✅ Fallback to "other" category
-- ✅ Empty category removal
-
-**Text Cleaning:**
-- ✅ Multiple spaces normalization
-- ✅ Newline normalization
-- ✅ Tab normalization
-- ✅ Whitespace stripping
-
-**Example Test:**
-```python
-def test_detect_python_from_heuristics(self):
- """Test Python detection from code content"""
- html = 'import os\nfrom pathlib import Path'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- lang = self.converter.detect_language(elem, elem.get_text())
- self.assertEqual(lang, 'python')
-```
-
-**Running:**
-```bash
-python3 run_tests.py --suite features -v
-```
-
----
-
-### 3. Integration Tests (`test_integration.py`)
-
-Tests complete workflows and interactions between components.
-
-**Test Categories:**
-
-**Dry-Run Mode:**
-- ✅ No directories created in dry-run mode
-- ✅ Dry-run flag properly set
-- ✅ Normal mode creates directories
-
-**Config Loading:**
-- ✅ Load valid configuration files
-- ✅ Invalid JSON error handling
-- ✅ Nonexistent file error handling
-- ✅ Validation errors during load
-
-**Real Config Validation:**
-- ✅ Godot config validation
-- ✅ React config validation
-- ✅ Vue config validation
-- ✅ Django config validation
-- ✅ FastAPI config validation
-- ✅ Steam Economy config validation
-
-**URL Processing:**
-- ✅ URL normalization
-- ✅ Start URLs fallback to base_url
-- ✅ Multiple start URLs handling
-
-**Content Extraction:**
-- ✅ Empty content handling
-- ✅ Basic content extraction
-- ✅ Code sample extraction with language detection
-
-**Example Test:**
-```python
-def test_dry_run_no_directories_created(self):
- """Test that dry-run mode doesn't create directories"""
- converter = DocToSkillConverter(self.config, dry_run=True)
-
- data_dir = Path(f"output/{self.config['name']}_data")
- skill_dir = Path(f"output/{self.config['name']}")
-
- self.assertFalse(data_dir.exists())
- self.assertFalse(skill_dir.exists())
-```
-
-**Running:**
-```bash
-python3 run_tests.py --suite integration -v
-```
-
----
-
-### 4. PDF Extraction Tests (`test_pdf_extractor.py`) **NEW**
-
-Tests PDF content extraction functionality (B1.2-B1.5).
-
-**Note:** These tests require PyMuPDF (`pip install PyMuPDF`). They will be skipped if not installed.
-
-**Test Categories:**
-
-**Language Detection (5 tests):**
-- ✅ Python detection with confidence scoring
-- ✅ JavaScript detection with confidence
-- ✅ C++ detection with confidence
-- ✅ Unknown language returns low confidence
-- ✅ Confidence always between 0 and 1
-
-**Syntax Validation (5 tests):**
-- ✅ Valid Python syntax validation
-- ✅ Invalid Python indentation detection
-- ✅ Unbalanced brackets detection
-- ✅ Valid JavaScript syntax validation
-- ✅ Natural language fails validation
-
-**Quality Scoring (4 tests):**
-- ✅ Quality score between 0 and 10
-- ✅ High-quality code gets good score (>7)
-- ✅ Low-quality code gets low score (<4)
-- ✅ Quality considers multiple factors
-
-**Chapter Detection (4 tests):**
-- ✅ Detect chapters with numbers
-- ✅ Detect uppercase chapter headers
-- ✅ Detect section headings (e.g., "2.1")
-- ✅ Normal text not detected as chapter
-
-**Code Block Merging (2 tests):**
-- ✅ Merge code blocks split across pages
-- ✅ Don't merge different languages
-
-**Code Detection Methods (2 tests):**
-- ✅ Pattern-based detection (keywords)
-- ✅ Indent-based detection
-
-**Quality Filtering (1 test):**
-- ✅ Filter by minimum quality threshold
-
-**Example Test:**
-```python
-def test_detect_python_with_confidence(self):
- """Test Python detection returns language and confidence"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "def hello():\n print('world')\n return True"
-
- language, confidence = extractor.detect_language_from_code(code)
-
- self.assertEqual(language, "python")
- self.assertGreater(confidence, 0.7)
- self.assertLessEqual(confidence, 1.0)
-```
-
-**Running:**
-```bash
-python3 -m pytest tests/test_pdf_extractor.py -v
-```
-
----
-
-### 5. PDF Workflow Tests (`test_pdf_scraper.py`) **NEW**
-
-Tests PDF to skill conversion workflow (B1.6).
-
-**Note:** These tests require PyMuPDF (`pip install PyMuPDF`). They will be skipped if not installed.
-
-**Test Categories:**
-
-**PDFToSkillConverter (3 tests):**
-- ✅ Initialization with name and PDF path
-- ✅ Initialization with config file
-- ✅ Requires name or config_path
-
-**Categorization (3 tests):**
-- ✅ Categorize by keywords
-- ✅ Categorize by chapters
-- ✅ Handle missing chapters
-
-**Skill Building (3 tests):**
-- ✅ Create required directory structure
-- ✅ Create SKILL.md with metadata
-- ✅ Create reference files for categories
-
-**Code Block Handling (2 tests):**
-- ✅ Include code blocks in references
-- ✅ Prefer high-quality code
-
-**Image Handling (2 tests):**
-- ✅ Save images to assets directory
-- ✅ Reference images in markdown
-
-**Error Handling (3 tests):**
-- ✅ Handle missing PDF files
-- ✅ Handle invalid config JSON
-- ✅ Handle missing required config fields
-
-**JSON Workflow (2 tests):**
-- ✅ Load from extracted JSON
-- ✅ Build from JSON without extraction
-
-**Example Test:**
-```python
-def test_build_skill_creates_structure(self):
- """Test that build_skill creates required directory structure"""
- converter = self.PDFToSkillConverter(
- name="test_skill",
- pdf_path="test.pdf",
- output_dir=self.temp_dir
- )
-
- converter.extracted_data = {
- "pages": [{"page_number": 1, "text": "Test", "code_blocks": [], "images": []}],
- "total_pages": 1
- }
- converter.categories = {"test": [converter.extracted_data["pages"][0]]}
-
- converter.build_skill()
-
- skill_dir = Path(self.temp_dir) / "test_skill"
- self.assertTrue(skill_dir.exists())
- self.assertTrue((skill_dir / "references").exists())
- self.assertTrue((skill_dir / "scripts").exists())
- self.assertTrue((skill_dir / "assets").exists())
-```
-
-**Running:**
-```bash
-python3 -m pytest tests/test_pdf_scraper.py -v
-```
-
----
-
-### 6. PDF Advanced Features Tests (`test_pdf_advanced_features.py`) **NEW**
-
-Tests advanced PDF features (Priority 2 & 3).
-
-**Note:** These tests require PyMuPDF (`pip install PyMuPDF`). OCR tests also require pytesseract and Pillow. They will be skipped if not installed.
-
-**Test Categories:**
-
-**OCR Support (5 tests):**
-- ✅ OCR flag initialization
-- ✅ OCR disabled behavior
-- ✅ OCR only triggers for minimal text
-- ✅ Warning when pytesseract unavailable
-- ✅ OCR extraction triggered correctly
-
-**Password Protection (4 tests):**
-- ✅ Password parameter initialization
-- ✅ Encrypted PDF detection
-- ✅ Wrong password handling
-- ✅ Missing password error
-
-**Table Extraction (5 tests):**
-- ✅ Table extraction flag initialization
-- ✅ No extraction when disabled
-- ✅ Basic table extraction
-- ✅ Multiple tables per page
-- ✅ Error handling during extraction
-
-**Caching (5 tests):**
-- ✅ Cache initialization
-- ✅ Set and get cached values
-- ✅ Cache miss returns None
-- ✅ Caching can be disabled
-- ✅ Cache overwrite
-
-**Parallel Processing (4 tests):**
-- ✅ Parallel flag initialization
-- ✅ Disabled by default
-- ✅ Worker count auto-detection
-- ✅ Custom worker count
-
-**Integration (3 tests):**
-- ✅ Full initialization with all features
-- ✅ Various feature combinations
-- ✅ Page data includes tables
-
-**Example Test:**
-```python
-def test_table_extraction_basic(self):
- """Test basic table extraction"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.extract_tables = True
- extractor.verbose = False
-
- # Create mock table
- mock_table = Mock()
- mock_table.extract.return_value = [
- ["Header 1", "Header 2", "Header 3"],
- ["Data 1", "Data 2", "Data 3"]
- ]
- mock_table.bbox = (0, 0, 100, 100)
-
- mock_tables = Mock()
- mock_tables.tables = [mock_table]
-
- mock_page = Mock()
- mock_page.find_tables.return_value = mock_tables
-
- tables = extractor.extract_tables_from_page(mock_page)
-
- self.assertEqual(len(tables), 1)
- self.assertEqual(tables[0]['row_count'], 2)
- self.assertEqual(tables[0]['col_count'], 3)
-```
-
-**Running:**
-```bash
-python3 -m pytest tests/test_pdf_advanced_features.py -v
-```
-
----
-
-## Test Runner Features
-
-The custom test runner (`run_tests.py`) provides:
-
-### Colored Output
-- 🟢 Green for passing tests
-- 🔴 Red for failures and errors
-- 🟡 Yellow for skipped tests
-
-### Detailed Summary
-```
-======================================================================
-TEST SUMMARY
-======================================================================
-
-Total Tests: 70
-✓ Passed: 68
-✗ Failed: 2
-⊘ Skipped: 0
-
-Success Rate: 97.1%
-
-Test Breakdown by Category:
- TestConfigValidation: 28/30 passed
- TestURLValidation: 6/6 passed
- TestLanguageDetection: 10/10 passed
- TestPatternExtraction: 3/3 passed
- TestCategorization: 5/5 passed
- TestDryRunMode: 3/3 passed
- TestConfigLoading: 4/4 passed
- TestRealConfigFiles: 6/6 passed
- TestContentExtraction: 3/3 passed
-
-======================================================================
-```
-
-### Command-Line Options
-
-```bash
-# Verbose output (show each test name)
-python3 run_tests.py -v
-
-# Quiet output (minimal)
-python3 run_tests.py -q
-
-# Stop on first failure
-python3 run_tests.py --failfast
-
-# Run specific suite
-python3 run_tests.py --suite config
-
-# List all tests
-python3 run_tests.py --list
-```
-
----
-
-## Running Individual Tests
-
-### Run Single Test File
-```bash
-python3 -m unittest tests.test_config_validation
-python3 -m unittest tests.test_scraper_features
-python3 -m unittest tests.test_integration
-```
-
-### Run Single Test Class
-```bash
-python3 -m unittest tests.test_config_validation.TestConfigValidation
-python3 -m unittest tests.test_scraper_features.TestLanguageDetection
-```
-
-### Run Single Test Method
-```bash
-python3 -m unittest tests.test_config_validation.TestConfigValidation.test_valid_complete_config
-python3 -m unittest tests.test_scraper_features.TestLanguageDetection.test_detect_python_from_heuristics
-```
-
----
-
-## Test Coverage
-
-### Current Coverage
-
-| Component | Tests | Coverage |
-|-----------|-------|----------|
-| Config Validation | 30+ | 100% |
-| URL Validation | 6 | 95% |
-| Language Detection | 10 | 90% |
-| Pattern Extraction | 3 | 85% |
-| Categorization | 5 | 90% |
-| Text Cleaning | 4 | 100% |
-| Dry-Run Mode | 3 | 100% |
-| Config Loading | 4 | 95% |
-| Real Configs | 6 | 100% |
-| Content Extraction | 3 | 80% |
-| **PDF Extraction** | **23** | **90%** |
-| **PDF Workflow** | **18** | **85%** |
-| **PDF Advanced Features** | **26** | **95%** |
-
-**Total: 142 tests (75 passing + 67 PDF tests)**
-
-**Note:** PDF tests (67 total) require PyMuPDF and will be skipped if not installed. When PyMuPDF is available, all 142 tests run.
-
-### Not Yet Covered
-- Network operations (actual scraping)
-- Enhancement scripts (`enhance_skill.py`, `enhance_skill_local.py`)
-- Package creation (`package_skill.py`)
-- Interactive mode
-- SKILL.md generation
-- Reference file creation
-- PDF extraction with real PDF files (tests use mocked data)
-
----
-
-## Writing New Tests
-
-### Test Template
-
-```python
-#!/usr/bin/env python3
-"""
-Test suite for [feature name]
-Tests [description of what's being tested]
-"""
-
-import sys
-import os
-import unittest
-
-# Add parent directory to path
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from doc_scraper import DocToSkillConverter
-
-
-class TestYourFeature(unittest.TestCase):
- """Test [feature] functionality"""
-
- def setUp(self):
- """Set up test fixtures"""
- self.config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- self.converter = DocToSkillConverter(self.config, dry_run=True)
-
- def tearDown(self):
- """Clean up after tests"""
- pass
-
- def test_your_feature(self):
- """Test description"""
- # Arrange
- test_input = "something"
-
- # Act
- result = self.converter.some_method(test_input)
-
- # Assert
- self.assertEqual(result, expected_value)
-
-
-if __name__ == '__main__':
- unittest.main()
-```
-
-### Best Practices
-
-1. **Use descriptive test names**: `test_valid_name_formats` not `test1`
-2. **Follow AAA pattern**: Arrange, Act, Assert
-3. **One assertion per test** when possible
-4. **Test edge cases**: empty inputs, invalid inputs, boundary values
-5. **Use setUp/tearDown**: for common initialization and cleanup
-6. **Mock external dependencies**: don't make real network calls
-7. **Keep tests independent**: tests should not depend on each other
-8. **Use dry_run=True**: for converter tests to avoid file creation
-
----
-
-## Continuous Integration
-
-### GitHub Actions (Future)
-
-```yaml
-name: Tests
-
-on: [push, pull_request]
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
- with:
- python-version: '3.7'
- - run: pip install requests beautifulsoup4
- - run: python3 run_tests.py
-```
-
----
-
-## Troubleshooting
-
-### Tests Fail with Import Errors
-```bash
-# Make sure you're in the repository root
-cd /path/to/Skill_Seekers
-
-# Run tests from root directory
-python3 run_tests.py
-```
-
-### Tests Create Output Directories
-```bash
-# Clean up test artifacts
-rm -rf output/test-*
-
-# Make sure tests use dry_run=True
-# Check test setUp methods
-```
-
-### Specific Test Keeps Failing
-```bash
-# Run only that test with verbose output
-python3 -m unittest tests.test_config_validation.TestConfigValidation.test_name -v
-
-# Check the error message carefully
-# Verify test expectations match implementation
-```
-
----
-
-## Performance
-
-Test execution times:
-- **Config Validation**: ~0.1 seconds (30 tests)
-- **Scraper Features**: ~0.3 seconds (25 tests)
-- **Integration Tests**: ~0.5 seconds (15 tests)
-- **Total**: ~1 second (70 tests)
-
----
-
-## Contributing Tests
-
-When adding new features:
-
-1. Write tests **before** implementing the feature (TDD)
-2. Ensure tests cover:
- - ✅ Happy path (valid inputs)
- - ✅ Edge cases (empty, null, boundary values)
- - ✅ Error cases (invalid inputs)
-3. Run tests before committing:
- ```bash
- python3 run_tests.py
- ```
-4. Aim for >80% coverage for new code
-
----
-
-## Additional Resources
-
-- **unittest documentation**: https://docs.python.org/3/library/unittest.html
-- **pytest** (alternative): https://pytest.org/ (more powerful, but requires installation)
-- **Test-Driven Development**: https://en.wikipedia.org/wiki/Test-driven_development
-
----
-
-## Summary
-
-✅ **142 comprehensive tests** covering all major features (75 + 67 PDF)
-✅ **PDF support testing** with 67 tests for B1 tasks + Priority 2 & 3
-✅ **Colored test runner** with detailed summaries
-✅ **Fast execution** (~1 second for full suite)
-✅ **Easy to extend** with clear patterns and templates
-✅ **Good coverage** of critical paths
-
-**PDF Tests Status:**
-- 23 tests for PDF extraction (language detection, syntax validation, quality scoring, chapter detection)
-- 18 tests for PDF workflow (initialization, categorization, skill building, code/image handling)
-- **26 tests for advanced features (OCR, passwords, tables, parallel, caching)** NEW!
-- Tests are skipped gracefully when PyMuPDF is not installed
-- Full test coverage when PyMuPDF + optional dependencies are available
-
-**Advanced PDF Features Tested:**
-- ✅ OCR support for scanned PDFs (5 tests)
-- ✅ Password-protected PDFs (4 tests)
-- ✅ Table extraction (5 tests)
-- ✅ Parallel processing (4 tests)
-- ✅ Caching (5 tests)
-- ✅ Integration (3 tests)
-
-Run tests frequently to catch bugs early! 🚀
diff --git a/assets/repo/Skill_Seekers-development/docs/TEST_MCP_IN_CLAUDE_CODE.md b/assets/repo/Skill_Seekers-development/docs/TEST_MCP_IN_CLAUDE_CODE.md
deleted file mode 100644
index d8415a2..0000000
--- a/assets/repo/Skill_Seekers-development/docs/TEST_MCP_IN_CLAUDE_CODE.md
+++ /dev/null
@@ -1,342 +0,0 @@
-# Testing MCP Server in Claude Code
-
-This guide shows you how to test the Skill Seeker MCP server **through actual Claude Code** using the MCP protocol (not just Python function calls).
-
-## Important: What We Tested vs What You Need to Test
-
-### What I Tested (Python Direct Calls) ✅
-I tested the MCP server **functions** by calling them directly with Python:
-```python
-await server.list_configs_tool({})
-await server.generate_config_tool({...})
-```
-
-This verified the **code works**, but didn't test the **MCP protocol integration**.
-
-### What You Need to Test (Actual MCP Protocol) 🎯
-You need to test via **Claude Code** using the MCP protocol:
-```
-In Claude Code:
-> List all available configs
-> mcp__skill-seeker__list_configs
-```
-
-This verifies the **full integration** works.
-
-## Setup Instructions
-
-### Step 1: Configure Claude Code
-
-Create the MCP configuration file:
-
-```bash
-# Create config directory
-mkdir -p ~/.config/claude-code
-
-# Create/edit MCP configuration
-nano ~/.config/claude-code/mcp.json
-```
-
-Add this configuration (replace `/path/to/` with your actual path):
-
-```json
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "python3",
- "args": [
- "/mnt/1ece809a-2821-4f10-aecb-fcdf34760c0b/Git/Skill_Seekers/skill_seeker_mcp/server.py"
- ],
- "cwd": "/mnt/1ece809a-2821-4f10-aecb-fcdf34760c0b/Git/Skill_Seekers"
- }
- }
-}
-```
-
-Or use the setup script:
-```bash
-./setup_mcp.sh
-```
-
-### Step 2: Restart Claude Code
-
-**IMPORTANT:** Completely quit and restart Claude Code (don't just close the window).
-
-### Step 3: Verify MCP Server Loaded
-
-In Claude Code, check if the server loaded:
-
-```
-Show me all available MCP tools
-```
-
-You should see 6 tools with the prefix `mcp__skill-seeker__`:
-- `mcp__skill-seeker__list_configs`
-- `mcp__skill-seeker__generate_config`
-- `mcp__skill-seeker__validate_config`
-- `mcp__skill-seeker__estimate_pages`
-- `mcp__skill-seeker__scrape_docs`
-- `mcp__skill-seeker__package_skill`
-
-## Testing All 6 MCP Tools
-
-### Test 1: list_configs
-
-**In Claude Code, type:**
-```
-List all available Skill Seeker configs
-```
-
-**Or explicitly:**
-```
-Use mcp__skill-seeker__list_configs
-```
-
-**Expected Output:**
-```
-📋 Available Configs:
-
- • django.json
- • fastapi.json
- • godot.json
- • react.json
- • vue.json
- ...
-```
-
-### Test 2: generate_config
-
-**In Claude Code, type:**
-```
-Generate a config for Astro documentation at https://docs.astro.build with max 15 pages
-```
-
-**Or explicitly:**
-```
-Use mcp__skill-seeker__generate_config with:
-- name: astro-test
-- url: https://docs.astro.build
-- description: Astro framework testing
-- max_pages: 15
-```
-
-**Expected Output:**
-```
-✅ Config created: configs/astro-test.json
-```
-
-### Test 3: validate_config
-
-**In Claude Code, type:**
-```
-Validate the astro-test config
-```
-
-**Or explicitly:**
-```
-Use mcp__skill-seeker__validate_config for configs/astro-test.json
-```
-
-**Expected Output:**
-```
-✅ Config is valid!
- Name: astro-test
- Base URL: https://docs.astro.build
- Max pages: 15
-```
-
-### Test 4: estimate_pages
-
-**In Claude Code, type:**
-```
-Estimate pages for the astro-test config
-```
-
-**Or explicitly:**
-```
-Use mcp__skill-seeker__estimate_pages for configs/astro-test.json
-```
-
-**Expected Output:**
-```
-📊 ESTIMATION RESULTS
-Estimated Total: ~25 pages
-Recommended max_pages: 75
-```
-
-### Test 5: scrape_docs
-
-**In Claude Code, type:**
-```
-Scrape docs using the astro-test config
-```
-
-**Or explicitly:**
-```
-Use mcp__skill-seeker__scrape_docs with configs/astro-test.json
-```
-
-**Expected Output:**
-```
-✅ Skill built: output/astro-test/
-Scraped X pages
-Created Y categories
-```
-
-### Test 6: package_skill
-
-**In Claude Code, type:**
-```
-Package the astro-test skill
-```
-
-**Or explicitly:**
-```
-Use mcp__skill-seeker__package_skill for output/astro-test/
-```
-
-**Expected Output:**
-```
-✅ Package created: output/astro-test.zip
-Size: X KB
-```
-
-## Complete Workflow Test
-
-Test the entire workflow in Claude Code with natural language:
-
-```
-Step 1:
-> List all available configs
-
-Step 2:
-> Generate config for Svelte at https://svelte.dev/docs with description "Svelte framework" and max 20 pages
-
-Step 3:
-> Validate configs/svelte.json
-
-Step 4:
-> Estimate pages for configs/svelte.json
-
-Step 5:
-> Scrape docs using configs/svelte.json
-
-Step 6:
-> Package skill at output/svelte/
-```
-
-Expected result: `output/svelte.zip` ready to upload to Claude!
-
-## Troubleshooting
-
-### Issue: Tools Not Appearing
-
-**Symptoms:**
-- Claude Code doesn't recognize skill-seeker commands
-- No `mcp__skill-seeker__` tools listed
-
-**Solutions:**
-
-1. Check configuration exists:
- ```bash
- cat ~/.config/claude-code/mcp.json
- ```
-
-2. Verify server can start:
- ```bash
- cd /path/to/Skill_Seekers
- python3 skill_seeker_mcp/server.py
- # Should start without errors (Ctrl+C to exit)
- ```
-
-3. Check dependencies installed:
- ```bash
- pip3 list | grep mcp
- # Should show: mcp x.x.x
- ```
-
-4. Completely restart Claude Code (quit and reopen)
-
-5. Check Claude Code logs:
- - macOS: `~/Library/Logs/Claude Code/`
- - Linux: `~/.config/claude-code/logs/`
-
-### Issue: "Permission Denied"
-
-```bash
-chmod +x skill_seeker_mcp/server.py
-```
-
-### Issue: "Module Not Found"
-
-```bash
-pip3 install -r skill_seeker_mcp/requirements.txt
-pip3 install requests beautifulsoup4
-```
-
-## Verification Checklist
-
-Use this checklist to verify MCP integration:
-
-- [ ] Configuration file created at `~/.config/claude-code/mcp.json`
-- [ ] Repository path in config is absolute and correct
-- [ ] Python dependencies installed (`mcp`, `requests`, `beautifulsoup4`)
-- [ ] Server starts without errors when run manually
-- [ ] Claude Code completely restarted (quit and reopened)
-- [ ] Tools appear when asking "show me all MCP tools"
-- [ ] Tools have `mcp__skill-seeker__` prefix
-- [ ] Can list configs successfully
-- [ ] Can generate a test config
-- [ ] Can scrape and package a small skill
-
-## What Makes This Different from My Tests
-
-| What I Tested | What You Should Test |
-|---------------|---------------------|
-| Python function calls | Claude Code MCP protocol |
-| `await server.list_configs_tool({})` | Natural language in Claude Code |
-| Direct Python imports | Full MCP server integration |
-| Validates code works | Validates Claude Code integration |
-| Quick unit testing | Real-world usage testing |
-
-## Success Criteria
-
-✅ **MCP Integration is Working When:**
-
-1. You can ask Claude Code to "list all available configs"
-2. Claude Code responds with the actual config list
-3. You can generate, validate, scrape, and package skills
-4. All through natural language commands in Claude Code
-5. No Python code needed - just conversation!
-
-## Next Steps After Successful Testing
-
-Once MCP integration works:
-
-1. **Create your first skill:**
- ```
- > Generate config for TailwindCSS at https://tailwindcss.com/docs
- > Scrape docs using configs/tailwind.json
- > Package skill at output/tailwind/
- ```
-
-2. **Upload to Claude:**
- - Take the generated `.zip` file
- - Upload to Claude.ai
- - Start using your new skill!
-
-3. **Share feedback:**
- - Report any issues on GitHub
- - Share successful skills created
- - Suggest improvements
-
-## Reference
-
-- **Full Setup Guide:** [docs/MCP_SETUP.md](docs/MCP_SETUP.md)
-- **MCP Documentation:** [mcp/README.md](mcp/README.md)
-- **Main README:** [README.md](README.md)
-- **Setup Script:** `./setup_mcp.sh`
-
----
-
-**Important:** This document is for testing the **actual MCP protocol integration** with Claude Code, not just the Python functions. Make sure you're testing through Claude Code's UI, not Python scripts!
diff --git a/assets/repo/Skill_Seekers-development/docs/UNIFIED_SCRAPING.md b/assets/repo/Skill_Seekers-development/docs/UNIFIED_SCRAPING.md
deleted file mode 100644
index 27845aa..0000000
--- a/assets/repo/Skill_Seekers-development/docs/UNIFIED_SCRAPING.md
+++ /dev/null
@@ -1,633 +0,0 @@
-# Unified Multi-Source Scraping
-
-**Version:** 2.0 (Feature complete as of October 2025)
-
-## Overview
-
-Unified multi-source scraping allows you to combine knowledge from multiple sources into a single comprehensive Claude skill. Instead of choosing between documentation, GitHub repositories, or PDF manuals, you can now extract and intelligently merge information from all of them.
-
-## Why Unified Scraping?
-
-**The Problem**: Documentation and code often drift apart over time. Official docs might be outdated, missing features that exist in code, or documenting features that have been removed. Separately scraping docs and code creates two incomplete skills.
-
-**The Solution**: Unified scraping:
-- Extracts information from multiple sources (documentation, GitHub, PDFs)
-- **Detects conflicts** between documentation and actual code implementation
-- **Intelligently merges** conflicting information with transparency
-- **Highlights discrepancies** with inline warnings (⚠️)
-- Creates a single, comprehensive skill that shows the complete picture
-
-## Quick Start
-
-### 1. Create a Unified Config
-
-Create a config file with multiple sources:
-
-```json
-{
- "name": "react",
- "description": "Complete React knowledge from docs + codebase",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://react.dev/",
- "extract_api": true,
- "max_pages": 200
- },
- {
- "type": "github",
- "repo": "facebook/react",
- "include_code": true,
- "code_analysis_depth": "surface",
- "max_issues": 100
- }
- ]
-}
-```
-
-### 2. Scrape and Build
-
-```bash
-python3 cli/unified_scraper.py --config configs/react_unified.json
-```
-
-The tool will:
-1. ✅ **Phase 1**: Scrape all sources (docs + GitHub)
-2. ✅ **Phase 2**: Detect conflicts between sources
-3. ✅ **Phase 3**: Merge conflicts intelligently
-4. ✅ **Phase 4**: Build unified skill with conflict transparency
-
-### 3. Package and Upload
-
-```bash
-python3 cli/package_skill.py output/react/
-```
-
-## Config Format
-
-### Unified Config Structure
-
-```json
-{
- "name": "skill-name",
- "description": "When to use this skill",
- "merge_mode": "rule-based|claude-enhanced",
- "sources": [
- {
- "type": "documentation|github|pdf",
- ...source-specific fields...
- }
- ]
-}
-```
-
-### Documentation Source
-
-```json
-{
- "type": "documentation",
- "base_url": "https://docs.example.com/",
- "extract_api": true,
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [],
- "exclude": ["/blog/"]
- },
- "categories": {
- "getting_started": ["intro", "tutorial"],
- "api": ["api", "reference"]
- },
- "rate_limit": 0.5,
- "max_pages": 200
-}
-```
-
-### GitHub Source
-
-```json
-{
- "type": "github",
- "repo": "owner/repo",
- "github_token": "ghp_...",
- "include_issues": true,
- "max_issues": 100,
- "include_changelog": true,
- "include_releases": true,
- "include_code": true,
- "code_analysis_depth": "surface|deep|full",
- "file_patterns": [
- "src/**/*.js",
- "lib/**/*.ts"
- ]
-}
-```
-
-**Code Analysis Depth**:
-- `surface` (default): Basic structure, no code analysis
-- `deep`: Extract class/function signatures, parameters, return types
-- `full`: Complete AST analysis (expensive)
-
-### PDF Source
-
-```json
-{
- "type": "pdf",
- "path": "/path/to/manual.pdf",
- "extract_tables": false,
- "ocr": false,
- "password": "optional-password"
-}
-```
-
-## Conflict Detection
-
-The unified scraper automatically detects 4 types of conflicts:
-
-### 1. Missing in Documentation
-
-**Severity**: Medium
-**Description**: API exists in code but is not documented
-
-**Example**:
-```python
-# Code has this method:
-def move_local_x(self, delta: float, snap: bool = False) -> None:
- """Move node along local X axis"""
-
-# But documentation doesn't mention it
-```
-
-**Suggestion**: Add documentation for this API
-
-### 2. Missing in Code
-
-**Severity**: High
-**Description**: API is documented but not found in codebase
-
-**Example**:
-```python
-# Docs say:
-def rotate(angle: float) -> None
-
-# But code doesn't have this function
-```
-
-**Suggestion**: Update documentation to remove this API, or add it to codebase
-
-### 3. Signature Mismatch
-
-**Severity**: Medium-High
-**Description**: API exists in both but signatures differ
-
-**Example**:
-```python
-# Docs say:
-def move_local_x(delta: float)
-
-# Code has:
-def move_local_x(delta: float, snap: bool = False)
-```
-
-**Suggestion**: Update documentation to match actual signature
-
-### 4. Description Mismatch
-
-**Severity**: Low
-**Description**: Different descriptions/docstrings
-
-## Merge Modes
-
-### Rule-Based Merge (Default)
-
-Fast, deterministic merging using predefined rules:
-
-1. **If API only in docs** → Include with `[DOCS_ONLY]` tag
-2. **If API only in code** → Include with `[UNDOCUMENTED]` tag
-3. **If both match perfectly** → Include normally
-4. **If conflict exists** → Prefer code signature, keep docs description
-
-**When to use**:
-- Fast merging (< 1 second)
-- Automated workflows
-- You don't need human oversight
-
-**Example**:
-```bash
-python3 cli/unified_scraper.py --config config.json --merge-mode rule-based
-```
-
-### Claude-Enhanced Merge
-
-AI-powered reconciliation using local Claude Code:
-
-1. Opens new terminal with Claude Code
-2. Provides conflict context and instructions
-3. Claude analyzes and creates reconciled API reference
-4. Human can review and adjust before finalizing
-
-**When to use**:
-- Complex conflicts requiring judgment
-- You want highest quality merge
-- You have time for human oversight
-
-**Example**:
-```bash
-python3 cli/unified_scraper.py --config config.json --merge-mode claude-enhanced
-```
-
-## Skill Output Structure
-
-The unified scraper creates this structure:
-
-```
-output/skill-name/
-├── SKILL.md # Main skill file with merged APIs
-├── references/
-│ ├── documentation/ # Documentation references
-│ │ └── index.md
-│ ├── github/ # GitHub references
-│ │ ├── README.md
-│ │ ├── issues.md
-│ │ └── releases.md
-│ ├── pdf/ # PDF references (if applicable)
-│ │ └── index.md
-│ ├── api/ # Merged API reference
-│ │ └── merged_api.md
-│ └── conflicts.md # Detailed conflict report
-├── scripts/ # Empty (for user scripts)
-└── assets/ # Empty (for user assets)
-```
-
-### SKILL.md Format
-
-```markdown
-# React
-
-Complete React knowledge base combining official documentation and React codebase insights.
-
-## 📚 Sources
-
-This skill combines knowledge from multiple sources:
-
-- ✅ **Documentation**: https://react.dev/
- - Pages: 200
-- ✅ **GitHub Repository**: facebook/react
- - Code Analysis: surface
- - Issues: 100
-
-## ⚠️ Data Quality
-
-**5 conflicts detected** between sources.
-
-**Conflict Breakdown:**
-- missing_in_docs: 3
-- missing_in_code: 2
-
-See `references/conflicts.md` for detailed conflict information.
-
-## 🔧 API Reference
-
-*Merged from documentation and code analysis*
-
-### ✅ Verified APIs
-
-*Documentation and code agree*
-
-#### `useState(initialValue)`
-
-...
-
-### ⚠️ APIs with Conflicts
-
-*Documentation and code differ*
-
-#### `useEffect(callback, deps?)`
-
-⚠️ **Conflict**: Documentation signature differs from code implementation
-
-**Documentation says:**
-```
-useEffect(callback: () => void, deps: any[])
-```
-
-**Code implementation:**
-```
-useEffect(callback: () => void | (() => void), deps?: readonly any[])
-```
-
-*Source: both*
-
----
-```
-
-## Examples
-
-### Example 1: React (Docs + GitHub)
-
-```json
-{
- "name": "react",
- "description": "Complete React framework knowledge",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://react.dev/",
- "extract_api": true,
- "max_pages": 200
- },
- {
- "type": "github",
- "repo": "facebook/react",
- "include_code": true,
- "code_analysis_depth": "surface"
- }
- ]
-}
-```
-
-### Example 2: Django (Docs + GitHub)
-
-```json
-{
- "name": "django",
- "description": "Complete Django framework knowledge",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://docs.djangoproject.com/en/stable/",
- "extract_api": true,
- "max_pages": 300
- },
- {
- "type": "github",
- "repo": "django/django",
- "include_code": true,
- "code_analysis_depth": "deep",
- "file_patterns": [
- "django/db/**/*.py",
- "django/views/**/*.py"
- ]
- }
- ]
-}
-```
-
-### Example 3: Mixed Sources (Docs + GitHub + PDF)
-
-```json
-{
- "name": "godot",
- "description": "Complete Godot Engine knowledge",
- "merge_mode": "claude-enhanced",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://docs.godotengine.org/en/stable/",
- "extract_api": true,
- "max_pages": 500
- },
- {
- "type": "github",
- "repo": "godotengine/godot",
- "include_code": true,
- "code_analysis_depth": "deep"
- },
- {
- "type": "pdf",
- "path": "/path/to/godot_manual.pdf",
- "extract_tables": true
- }
- ]
-}
-```
-
-## Command Reference
-
-### Unified Scraper
-
-```bash
-# Basic usage
-python3 cli/unified_scraper.py --config configs/react_unified.json
-
-# Override merge mode
-python3 cli/unified_scraper.py --config configs/react_unified.json --merge-mode claude-enhanced
-
-# Use cached data (skip re-scraping)
-python3 cli/unified_scraper.py --config configs/react_unified.json --skip-scrape
-```
-
-### Validate Config
-
-```bash
-python3 -c "
-import sys
-sys.path.insert(0, 'cli')
-from config_validator import validate_config
-
-validator = validate_config('configs/react_unified.json')
-print(f'Format: {\"Unified\" if validator.is_unified else \"Legacy\"}')
-print(f'Sources: {len(validator.config.get(\"sources\", []))}')
-print(f'Needs API merge: {validator.needs_api_merge()}')
-"
-```
-
-## MCP Integration
-
-The unified scraper is fully integrated with MCP. The `scrape_docs` tool automatically detects unified vs legacy configs and routes to the appropriate scraper.
-
-```python
-# MCP tool usage
-{
- "name": "scrape_docs",
- "arguments": {
- "config_path": "configs/react_unified.json",
- "merge_mode": "rule-based" # Optional override
- }
-}
-```
-
-The tool will:
-1. Auto-detect unified format
-2. Route to `unified_scraper.py`
-3. Apply specified merge mode
-4. Return comprehensive output
-
-## Backward Compatibility
-
-**Legacy configs still work!** The system automatically detects legacy single-source configs and routes to the original `doc_scraper.py`.
-
-```json
-// Legacy config (still works)
-{
- "name": "react",
- "base_url": "https://react.dev/",
- ...
-}
-
-// Automatically detected as legacy format
-// Routes to doc_scraper.py
-```
-
-## Testing
-
-Run integration tests:
-
-```bash
-python3 cli/test_unified_simple.py
-```
-
-Tests validate:
-- ✅ Unified config validation
-- ✅ Backward compatibility with legacy configs
-- ✅ Mixed source type support
-- ✅ Error handling for invalid configs
-
-## Architecture
-
-### Components
-
-1. **config_validator.py**: Validates unified and legacy configs
-2. **code_analyzer.py**: Extracts code signatures at configurable depth
-3. **conflict_detector.py**: Detects API conflicts between sources
-4. **merge_sources.py**: Implements rule-based and Claude-enhanced merging
-5. **unified_scraper.py**: Main orchestrator
-6. **unified_skill_builder.py**: Generates final skill structure
-7. **skill_seeker_mcp/server.py**: MCP integration with auto-detection
-
-### Data Flow
-
-```
-Unified Config
- ↓
-ConfigValidator (validates format)
- ↓
-UnifiedScraper.run()
- ↓
-┌────────────────────────────────────┐
-│ Phase 1: Scrape All Sources │
-│ - Documentation → doc_scraper │
-│ - GitHub → github_scraper │
-│ - PDF → pdf_scraper │
-└────────────────────────────────────┘
- ↓
-┌────────────────────────────────────┐
-│ Phase 2: Detect Conflicts │
-│ - ConflictDetector │
-│ - Compare docs APIs vs code APIs │
-│ - Classify by type and severity │
-└────────────────────────────────────┘
- ↓
-┌────────────────────────────────────┐
-│ Phase 3: Merge Sources │
-│ - RuleBasedMerger (fast) │
-│ - OR ClaudeEnhancedMerger (AI) │
-│ - Create unified API reference │
-└────────────────────────────────────┘
- ↓
-┌────────────────────────────────────┐
-│ Phase 4: Build Skill │
-│ - UnifiedSkillBuilder │
-│ - Generate SKILL.md with conflicts│
-│ - Create reference structure │
-│ - Generate conflicts report │
-└────────────────────────────────────┘
- ↓
-Unified Skill (.zip ready)
-```
-
-## Best Practices
-
-### 1. Start with Rule-Based Merge
-
-Rule-based is fast and works well for most cases. Only use Claude-enhanced if you need human oversight.
-
-### 2. Use Surface-Level Code Analysis
-
-`code_analysis_depth: "surface"` is usually sufficient. Deep analysis is expensive and rarely needed.
-
-### 3. Limit GitHub Issues
-
-`max_issues: 100` is a good default. More than 200 issues rarely adds value.
-
-### 4. Be Specific with File Patterns
-
-```json
-"file_patterns": [
- "src/**/*.js", // Good: specific paths
- "lib/**/*.ts"
-]
-
-// Not recommended:
-"file_patterns": ["**/*.js"] // Too broad, slow
-```
-
-### 5. Monitor Conflict Reports
-
-Always review `references/conflicts.md` to understand discrepancies between sources.
-
-## Troubleshooting
-
-### No Conflicts Detected
-
-**Possible causes**:
-- `extract_api: false` in documentation source
-- `include_code: false` in GitHub source
-- Code analysis found no APIs (check `code_analysis_depth`)
-
-**Solution**: Ensure both sources have API extraction enabled
-
-### Too Many Conflicts
-
-**Possible causes**:
-- Fuzzy matching threshold too strict
-- Documentation uses different naming conventions
-- Old documentation version
-
-**Solution**: Review conflicts manually and adjust merge strategy
-
-### Merge Takes Too Long
-
-**Possible causes**:
-- Using `code_analysis_depth: "full"` (very slow)
-- Too many file patterns
-- Large repository
-
-**Solution**:
-- Use `"surface"` or `"deep"` analysis
-- Narrow file patterns
-- Increase `rate_limit`
-
-## Future Enhancements
-
-Planned features:
-- [ ] Automated conflict resolution strategies
-- [ ] Conflict trend analysis across versions
-- [ ] Multi-version comparison (docs v1 vs v2)
-- [ ] Custom merge rules DSL
-- [ ] Conflict confidence scores
-
-## Support
-
-For issues, questions, or suggestions:
-- GitHub Issues: https://github.com/yusufkaraaslan/Skill_Seekers/issues
-- Documentation: https://github.com/yusufkaraaslan/Skill_Seekers/docs
-
-## Changelog
-
-**v2.0 (October 2025)**: Unified multi-source scraping feature complete
-- ✅ Config validation for unified format
-- ✅ Deep code analysis with AST parsing
-- ✅ Conflict detection (4 types, 3 severity levels)
-- ✅ Rule-based merging
-- ✅ Claude-enhanced merging
-- ✅ Unified skill builder with inline conflict warnings
-- ✅ MCP integration with auto-detection
-- ✅ Backward compatibility with legacy configs
-- ✅ Comprehensive tests and documentation
diff --git a/assets/repo/Skill_Seekers-development/docs/UPLOAD_GUIDE.md b/assets/repo/Skill_Seekers-development/docs/UPLOAD_GUIDE.md
deleted file mode 100644
index 25ad04c..0000000
--- a/assets/repo/Skill_Seekers-development/docs/UPLOAD_GUIDE.md
+++ /dev/null
@@ -1,351 +0,0 @@
-# How to Upload Skills to Claude
-
-## Quick Answer
-
-**You have 3 options to upload the `.zip` file:**
-
-### Option 1: Automatic Upload (Recommended for CLI)
-
-```bash
-# Set your API key (one-time setup)
-export ANTHROPIC_API_KEY=sk-ant-...
-
-# Package and upload automatically
-python3 cli/package_skill.py output/react/ --upload
-
-# OR upload existing .zip
-python3 cli/upload_skill.py output/react.zip
-```
-
-✅ **Fully automatic** | No manual steps | Requires API key
-
-### Option 2: Manual Upload (No API Key)
-
-```bash
-# Package the skill
-python3 cli/package_skill.py output/react/
-
-# This will:
-# 1. Create output/react.zip
-# 2. Open output/ folder automatically
-# 3. Show clear upload instructions
-
-# Then upload manually to https://claude.ai/skills
-```
-
-✅ **No API key needed** | Works for everyone | Simple
-
-### Option 3: Claude Code MCP (Easiest)
-
-```
-In Claude Code, just say:
-"Package and upload the React skill"
-
-# Automatically packages and uploads!
-```
-
-✅ **Natural language** | Fully automatic | Best UX
-
----
-
-## What's Inside the Zip?
-
-The `.zip` file contains:
-
-```
-steam-economy.zip
-├── SKILL.md ← Main skill file (Claude reads this first)
-└── references/ ← Reference documentation
- ├── index.md ← Category index
- ├── api_reference.md ← API docs
- ├── pricing.md ← Pricing docs
- ├── trading.md ← Trading docs
- └── ... ← Other categorized docs
-```
-
-**Note:** The zip only includes what Claude needs. It excludes:
-- `.backup` files
-- Build artifacts
-- Temporary files
-
-## What Does package_skill.py Do?
-
-The package script:
-
-1. **Finds your skill directory** (e.g., `output/steam-economy/`)
-2. **Validates SKILL.md exists** (required!)
-3. **Creates a .zip file** with the same name
-4. **Includes all files** except backups
-5. **Saves to** `output/` directory
-
-**Example:**
-```bash
-python3 cli/package_skill.py output/steam-economy/
-
-📦 Packaging skill: steam-economy
- Source: output/steam-economy
- Output: output/steam-economy.zip
- + SKILL.md
- + references/api_reference.md
- + references/pricing.md
- + references/trading.md
- + ...
-
-✅ Package created: output/steam-economy.zip
- Size: 14,290 bytes (14.0 KB)
-```
-
-## Complete Workflow
-
-### Step 1: Scrape & Build
-```bash
-python3 cli/doc_scraper.py --config configs/steam-economy.json
-```
-
-**Output:**
-- `output/steam-economy_data/` (raw scraped data)
-- `output/steam-economy/` (skill directory)
-
-### Step 2: Enhance (Recommended)
-```bash
-python3 cli/enhance_skill_local.py output/steam-economy/
-```
-
-**What it does:**
-- Analyzes reference files
-- Creates comprehensive SKILL.md
-- Backs up original to SKILL.md.backup
-
-**Output:**
-- `output/steam-economy/SKILL.md` (enhanced)
-- `output/steam-economy/SKILL.md.backup` (original)
-
-### Step 3: Package
-```bash
-python3 cli/package_skill.py output/steam-economy/
-```
-
-**Output:**
-- `output/steam-economy.zip` ← **THIS IS WHAT YOU UPLOAD**
-
-### Step 4: Upload to Claude
-1. Go to Claude (claude.ai)
-2. Click "Add Skill" or skill upload button
-3. Select `output/steam-economy.zip`
-4. Done!
-
-## What Files Are Required?
-
-**Minimum required structure:**
-```
-your-skill/
-└── SKILL.md ← Required! Claude reads this first
-```
-
-**Recommended structure:**
-```
-your-skill/
-├── SKILL.md ← Main skill file (required)
-└── references/ ← Reference docs (highly recommended)
- ├── index.md
- └── *.md ← Category files
-```
-
-**Optional (can add manually):**
-```
-your-skill/
-├── SKILL.md
-├── references/
-├── scripts/ ← Helper scripts
-│ └── *.py
-└── assets/ ← Templates, examples
- └── *.txt
-```
-
-## File Size Limits
-
-The package script shows size after packaging:
-```
-✅ Package created: output/steam-economy.zip
- Size: 14,290 bytes (14.0 KB)
-```
-
-**Typical sizes:**
-- Small skill: 5-20 KB
-- Medium skill: 20-100 KB
-- Large skill: 100-500 KB
-
-Claude has generous size limits, so most documentation-based skills fit easily.
-
-## Quick Reference
-
-### Package a Skill
-```bash
-python3 cli/package_skill.py output/steam-economy/
-```
-
-### Package Multiple Skills
-```bash
-# Package all skills in output/
-for dir in output/*/; do
- if [ -f "$dir/SKILL.md" ]; then
- python3 cli/package_skill.py "$dir"
- fi
-done
-```
-
-### Check What's in a Zip
-```bash
-unzip -l output/steam-economy.zip
-```
-
-### Test a Packaged Skill Locally
-```bash
-# Extract to temp directory
-mkdir temp-test
-unzip output/steam-economy.zip -d temp-test/
-cat temp-test/SKILL.md
-```
-
-## Troubleshooting
-
-### "SKILL.md not found"
-```bash
-# Make sure you scraped and built first
-python3 cli/doc_scraper.py --config configs/steam-economy.json
-
-# Then package
-python3 cli/package_skill.py output/steam-economy/
-```
-
-### "Directory not found"
-```bash
-# Check what skills are available
-ls output/
-
-# Use correct path
-python3 cli/package_skill.py output/YOUR-SKILL-NAME/
-```
-
-### Zip is Too Large
-Most skills are small, but if yours is large:
-```bash
-# Check size
-ls -lh output/steam-economy.zip
-
-# If needed, check what's taking space
-unzip -l output/steam-economy.zip | sort -k1 -rn | head -20
-```
-
-Reference files are usually small. Large sizes often mean:
-- Many images (skills typically don't need images)
-- Large code examples (these are fine, just be aware)
-
-## What Does Claude Do With the Zip?
-
-When you upload a skill zip:
-
-1. **Claude extracts it**
-2. **Reads SKILL.md first** - This tells Claude:
- - When to activate this skill
- - What the skill does
- - Quick reference examples
- - How to navigate the references
-3. **Indexes reference files** - Claude can search through:
- - `references/*.md` files
- - Find specific APIs, examples, concepts
-4. **Activates automatically** - When you ask about topics matching the skill
-
-## Example: Using the Packaged Skill
-
-After uploading `steam-economy.zip`:
-
-**You ask:** "How do I implement microtransactions in my Steam game?"
-
-**Claude:**
-- Recognizes this matches steam-economy skill
-- Reads SKILL.md for quick reference
-- Searches references/microtransactions.md
-- Provides detailed answer with code examples
-
-## API-Based Automatic Upload
-
-### Setup (One-Time)
-
-```bash
-# Get your API key from https://console.anthropic.com/
-export ANTHROPIC_API_KEY=sk-ant-...
-
-# Add to your shell profile to persist
-echo 'export ANTHROPIC_API_KEY=sk-ant-...' >> ~/.bashrc # or ~/.zshrc
-```
-
-### Usage
-
-```bash
-# Upload existing .zip
-python3 cli/upload_skill.py output/react.zip
-
-# OR package and upload in one command
-python3 cli/package_skill.py output/react/ --upload
-```
-
-### How It Works
-
-The upload tool uses the Anthropic `/v1/skills` API endpoint to:
-1. Read your .zip file
-2. Authenticate with your API key
-3. Upload to Claude's skill storage
-4. Verify upload success
-
-### Troubleshooting
-
-**"ANTHROPIC_API_KEY not set"**
-```bash
-# Check if set
-echo $ANTHROPIC_API_KEY
-
-# If empty, set it
-export ANTHROPIC_API_KEY=sk-ant-...
-```
-
-**"Authentication failed"**
-- Verify your API key is correct
-- Check https://console.anthropic.com/ for valid keys
-
-**"Upload timed out"**
-- Check your internet connection
-- Try again or use manual upload
-
-**Upload fails with error**
-- Falls back to showing manual upload instructions
-- You can still upload via https://claude.ai/skills
-
----
-
-## Summary
-
-**What you need to do:**
-
-### With API Key (Automatic):
-1. ✅ Scrape: `python3 cli/doc_scraper.py --config configs/YOUR-CONFIG.json`
-2. ✅ Enhance: `python3 cli/enhance_skill_local.py output/YOUR-SKILL/`
-3. ✅ Package & Upload: `python3 cli/package_skill.py output/YOUR-SKILL/ --upload`
-4. ✅ Done! Skill is live in Claude
-
-### Without API Key (Manual):
-1. ✅ Scrape: `python3 cli/doc_scraper.py --config configs/YOUR-CONFIG.json`
-2. ✅ Enhance: `python3 cli/enhance_skill_local.py output/YOUR-SKILL/`
-3. ✅ Package: `python3 cli/package_skill.py output/YOUR-SKILL/`
-4. ✅ Upload: Go to https://claude.ai/skills and upload the `.zip`
-
-**What you upload:**
-- The `.zip` file from `output/` directory
-- Example: `output/steam-economy.zip`
-
-**What's in the zip:**
-- `SKILL.md` (required)
-- `references/*.md` (recommended)
-- Any scripts/assets you added (optional)
-
-That's it! 🚀
diff --git a/assets/repo/Skill_Seekers-development/docs/USAGE.md b/assets/repo/Skill_Seekers-development/docs/USAGE.md
deleted file mode 100644
index 7e8bb14..0000000
--- a/assets/repo/Skill_Seekers-development/docs/USAGE.md
+++ /dev/null
@@ -1,811 +0,0 @@
-# Complete Usage Guide for Skill Seeker
-
-Comprehensive reference for all commands, options, and workflows.
-
-## Table of Contents
-
-- [Quick Reference](#quick-reference)
-- [Main Tool: doc_scraper.py](#main-tool-doc_scraperpy)
-- [Estimator: estimate_pages.py](#estimator-estimate_pagespy)
-- [Enhancement Tools](#enhancement-tools)
-- [Packaging Tool](#packaging-tool)
-- [Testing Tools](#testing-tools)
-- [Available Configs](#available-configs)
-- [Common Workflows](#common-workflows)
-- [Troubleshooting](#troubleshooting)
-
----
-
-## Quick Reference
-
-```bash
-# 1. Estimate pages (fast, 1-2 min)
-python3 cli/estimate_pages.py configs/react.json
-
-# 2. Scrape documentation (20-40 min)
-python3 cli/doc_scraper.py --config configs/react.json
-
-# 3. Enhance with Claude Code (60 sec)
-python3 cli/enhance_skill_local.py output/react/
-
-# 4. Package to .zip (instant)
-python3 cli/package_skill.py output/react/
-
-# 5. Test everything (1 sec)
-python3 cli/run_tests.py
-```
-
----
-
-## Main Tool: doc_scraper.py
-
-### Full Help
-
-```
-usage: doc_scraper.py [-h] [--interactive] [--config CONFIG] [--name NAME]
- [--url URL] [--description DESCRIPTION] [--skip-scrape]
- [--dry-run] [--enhance] [--enhance-local]
- [--api-key API_KEY]
-
-Convert documentation websites to Claude skills
-
-options:
- -h, --help Show this help message and exit
- --interactive, -i Interactive configuration mode
- --config, -c CONFIG Load configuration from file (e.g., configs/godot.json)
- --name NAME Skill name
- --url URL Base documentation URL
- --description, -d DESCRIPTION
- Skill description
- --skip-scrape Skip scraping, use existing data
- --dry-run Preview what will be scraped without actually scraping
- --enhance Enhance SKILL.md using Claude API after building
- (requires API key)
- --enhance-local Enhance SKILL.md using Claude Code in new terminal
- (no API key needed)
- --api-key API_KEY Anthropic API key for --enhance (or set ANTHROPIC_API_KEY)
-```
-
-### Usage Examples
-
-**1. Use Preset Config (Recommended)**
-```bash
-python3 cli/doc_scraper.py --config configs/godot.json
-python3 cli/doc_scraper.py --config configs/react.json
-python3 cli/doc_scraper.py --config configs/vue.json
-python3 cli/doc_scraper.py --config configs/django.json
-python3 cli/doc_scraper.py --config configs/fastapi.json
-```
-
-**2. Interactive Mode**
-```bash
-python3 cli/doc_scraper.py --interactive
-# Wizard walks you through:
-# - Skill name
-# - Base URL
-# - Description
-# - Selectors (optional)
-# - URL patterns (optional)
-# - Rate limit
-# - Max pages
-```
-
-**3. Quick Mode (Minimal)**
-```bash
-python3 cli/doc_scraper.py \
- --name react \
- --url https://react.dev/ \
- --description "React framework for building UIs"
-```
-
-**4. Dry-Run (Preview)**
-```bash
-python3 cli/doc_scraper.py --config configs/react.json --dry-run
-# Shows what will be scraped without downloading data
-# No directories created
-# Fast validation
-```
-
-**5. Skip Scraping (Use Cached Data)**
-```bash
-python3 cli/doc_scraper.py --config configs/godot.json --skip-scrape
-# Uses existing output/godot_data/
-# Fast rebuild (1-3 minutes)
-# Useful for testing changes
-```
-
-**6. With Local Enhancement**
-```bash
-python3 cli/doc_scraper.py --config configs/react.json --enhance-local
-# Scrapes + enhances in one command
-# Opens new terminal for Claude Code
-# No API key needed
-```
-
-**7. With API Enhancement**
-```bash
-export ANTHROPIC_API_KEY=sk-ant-...
-python3 cli/doc_scraper.py --config configs/react.json --enhance
-
-# Or with inline API key:
-python3 cli/doc_scraper.py --config configs/react.json --enhance --api-key sk-ant-...
-```
-
-### Output Structure
-
-```
-output/
-├── {name}_data/ # Scraped raw data (cached)
-│ ├── pages/
-│ │ ├── page_0.json
-│ │ ├── page_1.json
-│ │ └── ...
-│ └── summary.json # Scraping stats
-│
-└── {name}/ # Built skill directory
- ├── SKILL.md # Main skill file
- ├── SKILL.md.backup # Backup (if enhanced)
- ├── references/ # Categorized docs
- │ ├── index.md
- │ ├── getting_started.md
- │ ├── api.md
- │ └── ...
- ├── scripts/ # Empty (user scripts)
- └── assets/ # Empty (user assets)
-```
-
----
-
-## Estimator: estimate_pages.py
-
-### Full Help
-
-```
-usage: estimate_pages.py [-h] [--max-discovery MAX_DISCOVERY]
- [--timeout TIMEOUT]
- config
-
-Estimate page count for Skill Seeker configs
-
-positional arguments:
- config Path to config JSON file
-
-options:
- -h, --help Show this help message and exit
- --max-discovery, -m MAX_DISCOVERY
- Maximum pages to discover (default: 1000)
- --timeout, -t TIMEOUT
- HTTP request timeout in seconds (default: 30)
-```
-
-### Usage Examples
-
-**1. Quick Estimate (100 pages)**
-```bash
-python3 cli/estimate_pages.py configs/react.json --max-discovery 100
-# Time: ~30-60 seconds
-# Good for: Quick validation
-```
-
-**2. Standard Estimate (1000 pages - default)**
-```bash
-python3 cli/estimate_pages.py configs/godot.json
-# Time: ~1-2 minutes
-# Good for: Most use cases
-```
-
-**3. Deep Estimate (2000 pages)**
-```bash
-python3 cli/estimate_pages.py configs/vue.json --max-discovery 2000
-# Time: ~3-5 minutes
-# Good for: Large documentation sites
-```
-
-**4. Custom Timeout**
-```bash
-python3 cli/estimate_pages.py configs/django.json --timeout 60
-# Useful for slow servers
-```
-
-### Output Example
-
-```
-🔍 Estimating pages for: react
-📍 Base URL: https://react.dev/
-🎯 Start URLs: 6
-⏱️ Rate limit: 0.5s
-🔢 Max discovery: 1000
-
-⏳ Discovered: 180 pages (1.3 pages/sec)
-
-======================================================================
-📊 ESTIMATION RESULTS
-======================================================================
-
-Config: react
-Base URL: https://react.dev/
-
-✅ Pages Discovered: 180
-⏳ Pages Pending: 50
-📈 Estimated Total: 230
-
-⏱️ Time Elapsed: 140.5s
-⚡ Discovery Rate: 1.28 pages/sec
-
-======================================================================
-💡 RECOMMENDATIONS
-======================================================================
-
-✅ Current max_pages (300) is sufficient
-
-⏱️ Estimated full scrape time: 1.9 minutes
- (Based on rate_limit: 0.5s)
-```
-
-**What It Shows:**
-- Estimated total pages to scrape
-- Whether current `max_pages` is sufficient
-- Recommended `max_pages` value
-- Estimated scraping time
-- Discovery rate (pages/sec)
-
----
-
-## Enhancement Tools
-
-### enhance_skill_local.py (Recommended)
-
-**No API key needed - uses Claude Code Max plan**
-
-```bash
-# Usage
-python3 cli/enhance_skill_local.py output/react/
-python3 cli/enhance_skill_local.py output/godot/
-
-# What it does:
-# 1. Reads SKILL.md and references/
-# 2. Opens new terminal with Claude Code
-# 3. Claude enhances SKILL.md
-# 4. Backs up original to SKILL.md.backup
-# 5. Saves enhanced version
-
-# Time: ~60 seconds
-# Cost: Free (uses your Claude Code Max plan)
-```
-
-### enhance_skill.py (Alternative)
-
-**Requires Anthropic API key**
-
-```bash
-# Install dependency first
-pip3 install anthropic
-
-# Usage with environment variable
-export ANTHROPIC_API_KEY=sk-ant-...
-python3 cli/enhance_skill.py output/react/
-
-# Usage with inline API key
-python3 cli/enhance_skill.py output/godot/ --api-key sk-ant-...
-
-# What it does:
-# 1. Reads SKILL.md and references/
-# 2. Calls Claude API (Sonnet 4)
-# 3. Enhances SKILL.md
-# 4. Backs up original to SKILL.md.backup
-# 5. Saves enhanced version
-
-# Time: ~30-60 seconds
-# Cost: ~$0.01-0.10 per skill (depending on size)
-```
-
----
-
-## Packaging Tool
-
-### package_skill.py
-
-```bash
-# Usage
-python3 cli/package_skill.py output/react/
-python3 cli/package_skill.py output/godot/
-
-# What it does:
-# 1. Validates SKILL.md exists
-# 2. Creates .zip with all skill files
-# 3. Saves to output/{name}.zip
-
-# Output:
-# output/react.zip
-# output/godot.zip
-
-# Time: Instant
-```
-
----
-
-## Testing Tools
-
-### run_tests.py
-
-```bash
-# Run all tests (default)
-python3 cli/run_tests.py
-# 71 tests, ~1 second
-
-# Verbose output
-python3 cli/run_tests.py -v
-python3 cli/run_tests.py --verbose
-
-# Quiet output
-python3 cli/run_tests.py -q
-python3 cli/run_tests.py --quiet
-
-# Stop on first failure
-python3 cli/run_tests.py -f
-python3 cli/run_tests.py --failfast
-
-# Run specific test suite
-python3 cli/run_tests.py --suite config
-python3 cli/run_tests.py --suite features
-python3 cli/run_tests.py --suite integration
-
-# List all tests
-python3 cli/run_tests.py --list
-```
-
-### Individual Tests
-
-```bash
-# Run single test file
-python3 -m unittest tests.test_config_validation
-python3 -m unittest tests.test_scraper_features
-python3 -m unittest tests.test_integration
-
-# Run single test class
-python3 -m unittest tests.test_config_validation.TestConfigValidation
-
-# Run single test method
-python3 -m unittest tests.test_config_validation.TestConfigValidation.test_valid_complete_config
-```
-
----
-
-## Available Configs
-
-### Preset Configs (Ready to Use)
-
-| Config | Framework | Pages | Description |
-|--------|-----------|-------|-------------|
-| `godot.json` | Godot Engine | ~500 | Game engine documentation |
-| `react.json` | React | ~300 | React framework docs |
-| `vue.json` | Vue.js | ~250 | Vue.js framework docs |
-| `django.json` | Django | ~400 | Django web framework |
-| `fastapi.json` | FastAPI | ~200 | FastAPI Python framework |
-| `steam-economy-complete.json` | Steam | ~100 | Steam Economy API docs |
-
-### View Config Details
-
-```bash
-# List all configs
-ls configs/
-
-# View config content
-cat configs/react.json
-python3 -m json.tool configs/godot.json
-```
-
-### Config Structure
-
-```json
-{
- "name": "react",
- "base_url": "https://react.dev/",
- "description": "React - JavaScript library for building UIs",
- "start_urls": [
- "https://react.dev/learn",
- "https://react.dev/reference/react",
- "https://react.dev/reference/react-dom"
- ],
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": ["/learn/", "/reference/"],
- "exclude": ["/blog/", "/community/"]
- },
- "categories": {
- "getting_started": ["learn", "tutorial", "intro"],
- "api": ["reference", "api", "hooks"],
- "guides": ["guide"]
- },
- "rate_limit": 0.5,
- "max_pages": 300
-}
-```
-
----
-
-## Common Workflows
-
-### Workflow 1: Use Preset (Fastest)
-
-```bash
-# 1. Estimate (optional, 1-2 min)
-python3 cli/estimate_pages.py configs/react.json
-
-# 2. Scrape with local enhancement (25 min)
-python3 cli/doc_scraper.py --config configs/react.json --enhance-local
-
-# 3. Package (instant)
-python3 cli/package_skill.py output/react/
-
-# Result: output/react.zip
-# Upload to Claude!
-```
-
-### Workflow 2: Custom Documentation
-
-```bash
-# 1. Create config
-cat > configs/my-docs.json << 'EOF'
-{
- "name": "my-docs",
- "base_url": "https://docs.example.com/",
- "description": "My documentation site",
- "rate_limit": 0.5,
- "max_pages": 200
-}
-EOF
-
-# 2. Estimate
-python3 cli/estimate_pages.py configs/my-docs.json
-
-# 3. Dry-run test
-python3 cli/doc_scraper.py --config configs/my-docs.json --dry-run
-
-# 4. Full scrape
-python3 cli/doc_scraper.py --config configs/my-docs.json
-
-# 5. Enhance
-python3 cli/enhance_skill_local.py output/my-docs/
-
-# 6. Package
-python3 cli/package_skill.py output/my-docs/
-```
-
-### Workflow 3: Interactive Mode
-
-```bash
-# 1. Start interactive wizard
-python3 cli/doc_scraper.py --interactive
-
-# 2. Answer prompts:
-# - Name: my-framework
-# - URL: https://framework.dev/
-# - Description: My favorite framework
-# - Selectors: (uses defaults)
-# - Rate limit: 0.5
-# - Max pages: 100
-
-# 3. Enhance
-python3 cli/enhance_skill_local.py output/my-framework/
-
-# 4. Package
-python3 cli/package_skill.py output/my-framework/
-```
-
-### Workflow 4: Quick Mode
-
-```bash
-python3 cli/doc_scraper.py \
- --name vue \
- --url https://vuejs.org/ \
- --description "Vue.js framework" \
- --enhance-local
-```
-
-### Workflow 5: Rebuild from Cache
-
-```bash
-# Already scraped once?
-# Skip re-scraping, just rebuild
-python3 cli/doc_scraper.py --config configs/godot.json --skip-scrape
-
-# Try new enhancement
-python3 cli/enhance_skill_local.py output/godot/
-
-# Re-package
-python3 cli/package_skill.py output/godot/
-```
-
-### Workflow 6: Testing New Config
-
-```bash
-# 1. Create test config with low max_pages
-cat > configs/test.json << 'EOF'
-{
- "name": "test-site",
- "base_url": "https://docs.test.com/",
- "max_pages": 20,
- "rate_limit": 0.1
-}
-EOF
-
-# 2. Estimate
-python3 cli/estimate_pages.py configs/test.json --max-discovery 50
-
-# 3. Dry-run
-python3 cli/doc_scraper.py --config configs/test.json --dry-run
-
-# 4. Small scrape
-python3 cli/doc_scraper.py --config configs/test.json
-
-# 5. Validate output
-ls output/test-site/
-ls output/test-site/references/
-
-# 6. If good, increase max_pages and re-run
-```
-
----
-
-## Troubleshooting
-
-### Issue: "Rate limit exceeded"
-
-```bash
-# Increase rate_limit in config
-# Default: 0.5 seconds
-# Conservative: 1.0 seconds
-# Very conservative: 2.0 seconds
-
-# Edit config:
-{
- "rate_limit": 1.0
-}
-```
-
-### Issue: "Too many pages"
-
-```bash
-# Estimate first
-python3 cli/estimate_pages.py configs/my-config.json
-
-# Set max_pages based on estimate
-# Add buffer: estimated + 50
-
-# Edit config:
-{
- "max_pages": 350 # for 300 estimated
-}
-```
-
-### Issue: "No content extracted"
-
-```bash
-# Wrong selectors
-# Test selectors manually:
-curl -s https://docs.example.com/ | grep -i 'article\|main\|content'
-
-# Common selectors:
-"main_content": "article"
-"main_content": "main"
-"main_content": ".content"
-"main_content": "#main-content"
-"main_content": "div[role=\"main\"]"
-
-# Update config with correct selector
-```
-
-### Issue: "Tests failing"
-
-```bash
-# Run specific failing test
-python3 -m unittest tests.test_config_validation.TestConfigValidation.test_name -v
-
-# Check error message
-# Verify expectations match implementation
-```
-
-### Issue: "Enhancement fails"
-
-```bash
-# Local enhancement:
-# Make sure Claude Code is running
-# Check terminal output
-
-# API enhancement:
-# Verify API key is set:
-echo $ANTHROPIC_API_KEY
-
-# Or use inline:
-python3 cli/enhance_skill.py output/react/ --api-key sk-ant-...
-```
-
-### Issue: "Package fails"
-
-```bash
-# Verify SKILL.md exists
-ls output/my-skill/SKILL.md
-
-# If missing, build first:
-python3 cli/doc_scraper.py --config configs/my-skill.json --skip-scrape
-```
-
-### Issue: "Can't find output"
-
-```bash
-# Check output directory
-ls output/
-
-# Skill data (cached):
-ls output/{name}_data/
-
-# Built skill:
-ls output/{name}/
-
-# Packaged skill:
-ls output/{name}.zip
-```
-
----
-
-## Advanced Usage
-
-### Custom Selectors
-
-```json
-{
- "selectors": {
- "main_content": "div.documentation",
- "title": "h1.page-title",
- "code_blocks": "pre.highlight code",
- "navigation": "nav.sidebar"
- }
-}
-```
-
-### URL Pattern Filtering
-
-```json
-{
- "url_patterns": {
- "include": [
- "/docs/",
- "/guide/",
- "/api/",
- "/tutorial/"
- ],
- "exclude": [
- "/blog/",
- "/news/",
- "/community/",
- "/showcase/"
- ]
- }
-}
-```
-
-### Custom Categories
-
-```json
-{
- "categories": {
- "getting_started": ["intro", "tutorial", "quickstart", "installation"],
- "core_concepts": ["concept", "fundamental", "architecture"],
- "api": ["reference", "api", "method", "function"],
- "guides": ["guide", "how-to", "example"],
- "advanced": ["advanced", "expert", "performance"]
- }
-}
-```
-
-### Multiple Start URLs
-
-```json
-{
- "start_urls": [
- "https://docs.example.com/getting-started/",
- "https://docs.example.com/api/",
- "https://docs.example.com/guides/",
- "https://docs.example.com/examples/"
- ]
-}
-```
-
----
-
-## Performance Tips
-
-1. **Estimate first**: Save 20-40 minutes by validating config
-2. **Use dry-run**: Test selectors before full scrape
-3. **Cache data**: Use `--skip-scrape` for fast rebuilds
-4. **Adjust rate_limit**: Balance speed vs politeness
-5. **Set appropriate max_pages**: Don't scrape more than needed
-6. **Use start_urls**: Target specific documentation sections
-7. **Filter URLs**: Use include/exclude patterns
-8. **Run tests**: Catch issues early
-
----
-
-## Environment Variables
-
-```bash
-# Anthropic API key (for API enhancement)
-export ANTHROPIC_API_KEY=sk-ant-...
-
-# Optional: Set custom output directory
-export SKILL_SEEKER_OUTPUT_DIR=/path/to/output
-```
-
----
-
-## Exit Codes
-
-- `0`: Success
-- `1`: Error (general)
-- `2`: Warning (estimation hit limit)
-
----
-
-## File Locations
-
-```
-Skill_Seekers/
-├── doc_scraper.py # Main tool
-├── estimate_pages.py # Estimator
-├── enhance_skill.py # API enhancement
-├── enhance_skill_local.py # Local enhancement
-├── package_skill.py # Packager
-├── run_tests.py # Test runner
-├── configs/ # Preset configs
-├── tests/ # Test suite
-├── docs/ # Documentation
-└── output/ # Generated output
-```
-
----
-
-## Getting Help
-
-```bash
-# Tool-specific help
-python3 cli/doc_scraper.py --help
-python3 cli/estimate_pages.py --help
-python3 cli/run_tests.py --help
-
-# Documentation
-cat CLAUDE.md # Quick reference for Claude Code
-cat docs/CLAUDE.md # Detailed technical docs
-cat docs/TESTING.md # Testing guide
-cat docs/USAGE.md # This file
-cat docs/ENHANCEMENT.md # Enhancement guide
-cat docs/UPLOAD_GUIDE.md # Upload instructions
-cat README.md # Project overview
-```
-
----
-
-## Summary
-
-**Essential Commands:**
-```bash
-python3 cli/estimate_pages.py configs/react.json # Estimate
-python3 cli/doc_scraper.py --config configs/react.json # Scrape
-python3 cli/enhance_skill_local.py output/react/ # Enhance
-python3 cli/package_skill.py output/react/ # Package
-python3 cli/run_tests.py # Test
-```
-
-**Quick Start:**
-```bash
-pip3 install requests beautifulsoup4
-python3 cli/doc_scraper.py --config configs/react.json --enhance-local
-python3 cli/package_skill.py output/react/
-# Upload output/react.zip to Claude!
-```
-
-Happy skill creating! 🚀
diff --git a/assets/repo/Skill_Seekers-development/docs/plans/2025-10-24-active-skills-design.md b/assets/repo/Skill_Seekers-development/docs/plans/2025-10-24-active-skills-design.md
deleted file mode 100644
index 48b32e0..0000000
--- a/assets/repo/Skill_Seekers-development/docs/plans/2025-10-24-active-skills-design.md
+++ /dev/null
@@ -1,867 +0,0 @@
-# Active Skills Design - Demand-Driven Documentation Loading
-
-**Date:** 2025-10-24
-**Type:** Architecture Design
-**Status:** Phase 1 Implemented ✅
-**Author:** Edgar + Claude (Brainstorming Session)
-
----
-
-## Executive Summary
-
-Transform Skill_Seekers from creating **passive documentation dumps** into **active, intelligent skills** that load documentation on-demand. This eliminates context bloat (300k → 5-10k per query) while maintaining full access to complete documentation.
-
-**Key Innovation:** Skills become lightweight routers with heavy tools in `scripts/`, not documentation repositories.
-
----
-
-## Problem Statement
-
-### Current Architecture: Passive Skills
-
-**What happens today:**
-```
-Agent: "How do I use Hono middleware?"
- ↓
-Skill: *Claude loads 203k llms-txt.md into context*
- ↓
-Agent: *answers using loaded docs*
- ↓
-Result: Context bloat, slower performance, hits limits
-```
-
-**Issues:**
-1. **Context Bloat**: 319k llms-full.txt loaded entirely into context
-2. **Wasted Resources**: Agent needs 5k but gets 319k
-3. **Truncation Loss**: 36% of content lost (319k → 203k) due to size limits
-4. **File Extension Bug**: llms.txt files stored as .txt instead of .md
-5. **Single Variant**: Only downloads one file (usually llms-full.txt)
-
-### Current File Structure
-
-```
-output/hono/
-├── SKILL.md ──────────► Documentation dump + instructions
-├── references/
-│ └── llms-txt.md ───► 203k (36% truncated from 319k original)
-├── scripts/ ──────────► EMPTY (placeholder only!)
-└── assets/ ───────────► EMPTY (placeholder only!)
-```
-
----
-
-## Proposed Architecture: Active Skills
-
-### Core Concept
-
-**Skills = Routers + Tools**, not documentation dumps.
-
-**New workflow:**
-```
-Agent: "How do I use Hono middleware?"
- ↓
-Skill: *runs scripts/search.py "middleware"*
- ↓
-Script: *loads llms-full.md, extracts middleware section, returns 8k*
- ↓
-Agent: *answers using ONLY 8k* (CLEAN CONTEXT!)
- ↓
-Result: 40x less context, no truncation, full access to docs
-```
-
-### Benefits
-
-| Metric | Before | After | Improvement |
-|--------|--------|-------|-------------|
-| Context per query | 203k | 5-10k | **20-40x reduction** |
-| Content loss | 36% truncated | 0% (no truncation) | **Full fidelity** |
-| Variants available | 1 | 3 | **User choice** |
-| File format | .txt (wrong) | .md (correct) | **Fixed** |
-| Agent workflow | Passive read | Active tools | **Autonomous** |
-
----
-
-## Design Components
-
-### Component 1: Multi-Variant Download
-
-**Change:** Download ALL 3 variants, not just one.
-
-**File naming (FIXED):**
-- `https://hono.dev/llms-full.txt` → `llms-full.md` ✅
-- `https://hono.dev/llms.txt` → `llms.md` ✅
-- `https://hono.dev/llms-small.txt` → `llms-small.md` ✅
-
-**Sizes (Hono example):**
-- `llms-full.md` - 319k (complete documentation)
-- `llms-small.md` - 176k (curated essentials)
-- `llms.md` - 5.4k (quick reference)
-
-**Storage:**
-```
-output/hono/references/
-├── llms-full.md # 319k - everything (RENAMED from .txt)
-├── llms-small.md # 176k - curated (RENAMED from .txt)
-├── llms.md # 5.4k - quick ref (RENAMED from .txt)
-└── catalog.json # Generated index (NEW)
-```
-
-**Implementation in `_try_llms_txt()`:**
-```python
-def _try_llms_txt(self) -> bool:
- """Download ALL llms.txt variants for active skills"""
-
- # 1. Detect all available variants
- detector = LlmsTxtDetector(self.base_url)
- variants = detector.detect_all() # NEW method
-
- downloaded = {}
- for variant_info in variants:
- url = variant_info['url'] # https://hono.dev/llms-full.txt
- variant = variant_info['variant'] # 'full', 'standard', 'small'
-
- downloader = LlmsTxtDownloader(url)
- content = downloader.download()
-
- if content:
- # ✨ FIX: Rename .txt → .md immediately
- clean_name = f"llms-{variant}.md"
- downloaded[variant] = {
- 'content': content,
- 'filename': clean_name
- }
-
- # 2. Save ALL variants (not just one)
- for variant, data in downloaded.items():
- path = os.path.join(self.skill_dir, "references", data['filename'])
- with open(path, 'w', encoding='utf-8') as f:
- f.write(data['content'])
-
- # 3. Generate catalog from smallest variant
- if 'small' in downloaded:
- self._generate_catalog(downloaded['small']['content'])
-
- return True
-```
-
----
-
-### Component 2: The Catalog System
-
-**Purpose:** Lightweight index of what exists, not the content itself.
-
-**File:** `assets/catalog.json`
-
-**Structure:**
-```json
-{
- "metadata": {
- "framework": "hono",
- "version": "auto-detected",
- "generated": "2025-10-24T14:30:00Z",
- "total_sections": 93,
- "variants": {
- "quick": "llms-small.md",
- "standard": "llms.md",
- "complete": "llms-full.md"
- }
- },
- "sections": [
- {
- "id": "routing",
- "title": "Routing",
- "h1_marker": "# Routing",
- "topics": ["routes", "path", "params", "wildcard"],
- "size_bytes": 4800,
- "variants": ["quick", "complete"],
- "complexity": "beginner"
- },
- {
- "id": "middleware",
- "title": "Middleware",
- "h1_marker": "# Middleware",
- "topics": ["cors", "auth", "logging", "compression"],
- "size_bytes": 8200,
- "variants": ["quick", "complete"],
- "complexity": "intermediate"
- }
- ],
- "search_index": {
- "cors": ["middleware"],
- "routing": ["routing", "path-parameters"],
- "authentication": ["middleware", "jwt"],
- "context": ["context-handling"],
- "streaming": ["streaming-responses"]
- }
-}
-```
-
-**Generation (from llms-small.md):**
-```python
-def _generate_catalog(self, llms_small_content):
- """Generate catalog.json from llms-small.md TOC"""
- catalog = {
- "metadata": {...},
- "sections": [],
- "search_index": {}
- }
-
- # Split by h1 headers
- sections = re.split(r'\n# ', llms_small_content)
-
- for section_text in sections[1:]:
- lines = section_text.split('\n')
- title = lines[0].strip()
-
- # Extract h2 topics
- topics = re.findall(r'^## (.+)$', section_text, re.MULTILINE)
- topics = [t.strip().lower() for t in topics]
-
- section_info = {
- "id": title.lower().replace(' ', '-'),
- "title": title,
- "h1_marker": f"# {title}",
- "topics": topics + [title.lower()],
- "size_bytes": len(section_text),
- "variants": ["quick", "complete"]
- }
-
- catalog["sections"].append(section_info)
-
- # Build search index
- for topic in section_info["topics"]:
- if topic not in catalog["search_index"]:
- catalog["search_index"][topic] = []
- catalog["search_index"][topic].append(section_info["id"])
-
- # Save to assets/catalog.json
- catalog_path = os.path.join(self.skill_dir, "assets", "catalog.json")
- with open(catalog_path, 'w', encoding='utf-8') as f:
- json.dump(catalog, f, indent=2)
-```
-
----
-
-### Component 3: Active Scripts
-
-**Location:** `scripts/` directory (currently empty)
-
-#### Script 1: `scripts/search.py`
-
-**Purpose:** Search and return only relevant documentation sections.
-
-```python
-#!/usr/bin/env python3
-"""
-ABOUTME: Searches framework documentation and returns relevant sections
-ABOUTME: Loads only what's needed - keeps agent context clean
-"""
-
-import json
-import sys
-import re
-from pathlib import Path
-
-def search(query, detail="auto"):
- """
- Search documentation and return relevant sections.
-
- Args:
- query: Search term (e.g., "middleware", "cors", "routing")
- detail: "quick" | "standard" | "complete" | "auto"
-
- Returns:
- Markdown text of relevant sections only
- """
- # Load catalog
- catalog_path = Path(__file__).parent.parent / "assets" / "catalog.json"
- catalog = json.load(open(catalog_path))
-
- # 1. Find matching sections using search index
- query_lower = query.lower()
- matching_section_ids = set()
-
- for keyword, section_ids in catalog["search_index"].items():
- if query_lower in keyword or keyword in query_lower:
- matching_section_ids.update(section_ids)
-
- # Get section details
- matches = [s for s in catalog["sections"] if s["id"] in matching_section_ids]
-
- if not matches:
- return f"❌ No sections found for '{query}'. Try: python scripts/list_topics.py"
-
- # 2. Determine detail level
- if detail == "auto":
- # Use quick for overview, complete for deep dive
- total_size = sum(s["size_bytes"] for s in matches)
- if total_size > 50000: # > 50k
- variant = "quick"
- else:
- variant = "complete"
- else:
- variant = detail
-
- variant_file = catalog["metadata"]["variants"].get(variant, "complete")
-
- # 3. Load documentation file
- doc_path = Path(__file__).parent.parent / "references" / variant_file
- doc_content = open(doc_path, 'r', encoding='utf-8').read()
-
- # 4. Extract matched sections
- results = []
- for match in matches:
- h1_marker = match["h1_marker"]
-
- # Find section boundaries
- start = doc_content.find(h1_marker)
- if start == -1:
- continue
-
- # Find next h1 (or end of file)
- next_h1 = doc_content.find("\n# ", start + len(h1_marker))
- if next_h1 == -1:
- section_text = doc_content[start:]
- else:
- section_text = doc_content[start:next_h1]
-
- results.append({
- 'title': match['title'],
- 'size': len(section_text),
- 'content': section_text
- })
-
- # 5. Format output
- output = [f"# Search Results for '{query}' ({len(results)} sections found)\n"]
- output.append(f"**Variant used:** {variant} ({variant_file})")
- output.append(f"**Total size:** {sum(r['size'] for r in results):,} bytes\n")
- output.append("---\n")
-
- for result in results:
- output.append(result['content'])
- output.append("\n---\n")
-
- return '\n'.join(output)
-
-if __name__ == "__main__":
- if len(sys.argv) < 2:
- print("Usage: python search.py [detail]")
- print("Example: python search.py middleware")
- print("Example: python search.py routing --detail quick")
- sys.exit(1)
-
- query = sys.argv[1]
- detail = sys.argv[2] if len(sys.argv) > 2 else "auto"
-
- print(search(query, detail))
-```
-
-#### Script 2: `scripts/list_topics.py`
-
-**Purpose:** Show all available documentation sections.
-
-```python
-#!/usr/bin/env python3
-"""
-ABOUTME: Lists all available documentation sections with sizes
-ABOUTME: Helps agent discover what documentation exists
-"""
-
-import json
-from pathlib import Path
-
-def list_topics():
- """List all available documentation sections."""
- catalog_path = Path(__file__).parent.parent / "assets" / "catalog.json"
- catalog = json.load(open(catalog_path))
-
- print(f"# Available Documentation Topics ({catalog['metadata']['framework']})\n")
- print(f"**Total sections:** {catalog['metadata']['total_sections']}")
- print(f"**Variants:** {', '.join(catalog['metadata']['variants'].keys())}\n")
- print("---\n")
-
- # Group by complexity if available
- by_complexity = {}
- for section in catalog["sections"]:
- complexity = section.get("complexity", "general")
- if complexity not in by_complexity:
- by_complexity[complexity] = []
- by_complexity[complexity].append(section)
-
- for complexity in ["beginner", "intermediate", "advanced", "general"]:
- if complexity not in by_complexity:
- continue
-
- sections = by_complexity[complexity]
- print(f"## {complexity.title()} ({len(sections)} sections)\n")
-
- for section in sections:
- size_kb = section["size_bytes"] / 1024
- topics_str = ", ".join(section["topics"][:3])
- print(f"- **{section['title']}** ({size_kb:.1f}k)")
- print(f" Topics: {topics_str}")
- print(f" Search: `python scripts/search.py {section['id']}`\n")
-
-if __name__ == "__main__":
- list_topics()
-```
-
-#### Script 3: `scripts/get_section.py`
-
-**Purpose:** Extract a complete section by exact title.
-
-```python
-#!/usr/bin/env python3
-"""
-ABOUTME: Extracts a complete documentation section by title
-ABOUTME: Returns full section from llms-full.md (no truncation)
-"""
-
-import json
-import sys
-from pathlib import Path
-
-def get_section(title, variant="complete"):
- """
- Get a complete section by exact title.
-
- Args:
- title: Section title (e.g., "Middleware", "Routing")
- variant: Which file to use (quick/standard/complete)
-
- Returns:
- Complete section content
- """
- catalog_path = Path(__file__).parent.parent / "assets" / "catalog.json"
- catalog = json.load(open(catalog_path))
-
- # Find section
- section = None
- for s in catalog["sections"]:
- if s["title"].lower() == title.lower():
- section = s
- break
-
- if not section:
- return f"❌ Section '{title}' not found. Try: python scripts/list_topics.py"
-
- # Load doc
- variant_file = catalog["metadata"]["variants"].get(variant, "complete")
- doc_path = Path(__file__).parent.parent / "references" / variant_file
- doc_content = open(doc_path, 'r', encoding='utf-8').read()
-
- # Extract section
- h1_marker = section["h1_marker"]
- start = doc_content.find(h1_marker)
-
- if start == -1:
- return f"❌ Section '{title}' not found in {variant_file}"
-
- next_h1 = doc_content.find("\n# ", start + len(h1_marker))
- if next_h1 == -1:
- section_text = doc_content[start:]
- else:
- section_text = doc_content[start:next_h1]
-
- return section_text
-
-if __name__ == "__main__":
- if len(sys.argv) < 2:
- print("Usage: python get_section.py [variant]")
- print("Example: python get_section.py Middleware")
- print("Example: python get_section.py Routing quick")
- sys.exit(1)
-
- title = sys.argv[1]
- variant = sys.argv[2] if len(sys.argv) > 2 else "complete"
-
- print(get_section(title, variant))
-```
-
----
-
-### Component 4: Active SKILL.md Template
-
-**New template for llms.txt-based skills:**
-
-```markdown
----
-name: {name}
-description: {description}
-type: active
----
-
-# {Name} Skill
-
-**⚡ This is an ACTIVE skill** - Uses scripts to load documentation on-demand instead of dumping everything into context.
-
-## 🎯 Strategy: Demand-Driven Documentation
-
-**Traditional approach:**
-- Load 300k+ documentation into context
-- Agent reads everything to answer one question
-- Context bloat, slower performance
-
-**Active approach:**
-- Load 5-10k of relevant sections on-demand
-- Agent calls scripts to fetch what's needed
-- Clean context, faster performance
-
-## 📚 Available Documentation
-
-This skill provides access to {num_sections} documentation sections across 3 detail levels:
-
-- **Quick Reference** (`llms-small.md`): {small_size}k - Curated essentials
-- **Standard** (`llms.md`): {standard_size}k - Core concepts
-- **Complete** (`llms-full.md`): {full_size}k - Everything
-
-## 🔧 Tools Available
-
-### 1. Search Documentation
-Find and load only relevant sections:
-
-```bash
-python scripts/search.py "middleware"
-python scripts/search.py "routing" --detail quick
-```
-
-**Returns:** 5-10k of relevant content (not 300k!)
-
-### 2. List All Topics
-See what documentation exists:
-
-```bash
-python scripts/list_topics.py
-```
-
-**Returns:** Table of contents with section sizes and search hints
-
-### 3. Get Complete Section
-Extract a full section by title:
-
-```bash
-python scripts/get_section.py "Middleware"
-python scripts/get_section.py "Routing" quick
-```
-
-**Returns:** Complete section from chosen variant
-
-## 💡 Recommended Workflow
-
-1. **Discover:** `python scripts/list_topics.py` to see what's available
-2. **Search:** `python scripts/search.py "your topic"` to find relevant sections
-3. **Deep Dive:** Use returned content to answer questions in detail
-4. **Iterate:** Search more specific topics as needed
-
-## ⚠️ Important
-
-**DON'T:** Read `references/*.md` files directly into context
-**DO:** Use scripts to fetch only what you need
-
-This keeps your context clean and focused!
-
-## 📊 Index
-
-Complete section catalog available in `assets/catalog.json` with search mappings and size information.
-
-## 🔄 Updating
-
-To refresh with latest documentation:
-```bash
-python3 cli/doc_scraper.py --config configs/{name}.json
-```
-```
-
----
-
-## Implementation Plan
-
-### Phase 1: Foundation (Quick Fixes)
-
-**Tasks:**
-1. Fix `.txt` → `.md` renaming in downloader
-2. Download all 3 variants (not just one)
-3. Store all variants in `references/` with correct names
-4. Remove content truncation (2500 chars → unlimited)
-
-**Time:** 1-2 hours
-**Files:** `cli/doc_scraper.py`, `cli/llms_txt_downloader.py`
-
-### Phase 2: Catalog System
-
-**Tasks:**
-1. Implement `_generate_catalog()` method
-2. Parse llms-small.md to extract sections
-3. Build search index from topics
-4. Generate `assets/catalog.json`
-
-**Time:** 2-3 hours
-**Files:** `cli/doc_scraper.py`
-
-### Phase 3: Active Scripts
-
-**Tasks:**
-1. Create `scripts/search.py`
-2. Create `scripts/list_topics.py`
-3. Create `scripts/get_section.py`
-4. Make scripts executable (`chmod +x`)
-
-**Time:** 2-3 hours
-**Files:** New scripts in `scripts/` template directory
-
-### Phase 4: Template Updates
-
-**Tasks:**
-1. Create new active SKILL.md template
-2. Update `create_enhanced_skill_md()` to use active template for llms.txt skills
-3. Update documentation to explain active skills
-
-**Time:** 1 hour
-**Files:** `cli/doc_scraper.py`, `README.md`, `CLAUDE.md`
-
-### Phase 5: Testing & Refinement
-
-**Tasks:**
-1. Test with Hono skill (has all 3 variants)
-2. Test search accuracy
-3. Measure context reduction
-4. Document examples
-
-**Time:** 2-3 hours
-
-**Total Estimated Time:** 8-12 hours
-
----
-
-## Migration Path
-
-### Backward Compatibility
-
-**Existing skills:** No changes (passive skills still work)
-**New llms.txt skills:** Automatically use active architecture
-**User choice:** Can disable via config flag
-
-### Config Option
-
-```json
-{
- "name": "hono",
- "llms_txt_url": "https://hono.dev/llms-full.txt",
- "active_skill": true, // NEW: Enable active architecture (default: true)
- "base_url": "https://hono.dev/docs"
-}
-```
-
-### Detection Logic
-
-```python
-# In _try_llms_txt()
-active_mode = self.config.get('active_skill', True) # Default true
-
-if active_mode:
- # Download all variants, generate catalog, create scripts
- self._build_active_skill(downloaded)
-else:
- # Traditional: single file, no scripts
- self._build_passive_skill(downloaded)
-```
-
----
-
-## Benefits Analysis
-
-### Context Efficiency
-
-| Scenario | Passive Skill | Active Skill | Improvement |
-|----------|---------------|--------------|-------------|
-| Simple query | 203k loaded | 5k loaded | **40x reduction** |
-| Multi-topic query | 203k loaded | 15k loaded | **13x reduction** |
-| Deep dive | 203k loaded | 30k loaded | **6x reduction** |
-
-### Data Fidelity
-
-| Aspect | Passive | Active |
-|--------|---------|--------|
-| Content truncation | 36% lost | 0% lost |
-| Code truncation | 600 chars max | Unlimited |
-| Variants available | 1 | 3 |
-
-### Agent Capabilities
-
-**Passive Skills:**
-- ❌ Cannot choose detail level
-- ❌ Cannot search efficiently
-- ❌ Must read entire context
-- ❌ Limited by context window
-
-**Active Skills:**
-- ✅ Chooses appropriate detail level
-- ✅ Searches catalog efficiently
-- ✅ Loads only what's needed
-- ✅ Unlimited documentation access
-
----
-
-## Trade-offs
-
-### Advantages
-
-1. **Massive context reduction** (20-40x less per query)
-2. **No content loss** (all 3 variants preserved)
-3. **Correct file format** (.md not .txt)
-4. **Agent autonomy** (tools to fetch docs)
-5. **Scalable** (works with 1MB+ docs)
-
-### Disadvantages
-
-1. **Complexity** (scripts + catalog vs simple files)
-2. **Initial overhead** (catalog generation)
-3. **Agent learning curve** (must learn to use scripts)
-4. **Dependency** (Python required to run scripts)
-
-### Risk Mitigation
-
-**Risk:** Scripts don't work in Claude's sandbox
-**Mitigation:** Test thoroughly, provide fallback to passive mode
-
-**Risk:** Catalog generation fails
-**Mitigation:** Graceful degradation to single-file mode
-
-**Risk:** Agent doesn't use scripts
-**Mitigation:** Clear SKILL.md instructions, examples in quick reference
-
----
-
-## Success Metrics
-
-### Technical Metrics
-
-- ✅ Context per query < 20k (down from 203k)
-- ✅ All 3 variants downloaded and named correctly
-- ✅ 0% content truncation
-- ✅ Catalog generation < 5 seconds
-- ✅ Search script < 1 second response time
-
-### User Experience Metrics
-
-- ✅ Agent successfully uses scripts without prompting
-- ✅ Answers are equally or more accurate than passive mode
-- ✅ Agent can handle queries about all documentation sections
-- ✅ No "context limit exceeded" errors
-
----
-
-## Future Enhancements
-
-### Phase 6: Smart Caching
-
-Cache frequently accessed sections in SKILL.md quick reference:
-```python
-# Track access frequency in catalog.json
-"sections": [
- {
- "id": "middleware",
- "access_count": 47, # NEW: Track usage
- "last_accessed": "2025-10-24T14:30:00Z"
- }
-]
-
-# Include top 10 most-accessed sections directly in SKILL.md
-```
-
-### Phase 7: Semantic Search
-
-Use embeddings for better search:
-```python
-# Generate embeddings for each section
-"sections": [
- {
- "id": "middleware",
- "embedding": [...], # NEW: Vector embedding
- "topics": ["cors", "auth"]
- }
-]
-
-# In search.py: Use cosine similarity for better matches
-```
-
-### Phase 8: Progressive Loading
-
-Load increasingly detailed docs:
-```python
-# First: Load llms.md (5.4k - overview)
-# If insufficient: Load llms-small.md section (15k)
-# If still insufficient: Load llms-full.md section (30k)
-```
-
----
-
-## Conclusion
-
-Active skills represent a fundamental shift from **documentation repositories** to **documentation routers**. By treating skills as intelligent intermediaries rather than static dumps, we can:
-
-1. **Eliminate context bloat** (40x reduction)
-2. **Preserve full fidelity** (0% truncation)
-3. **Enable agent autonomy** (tools to fetch docs)
-4. **Scale indefinitely** (no size limits)
-
-This design maintains backward compatibility while unlocking new capabilities for modern, LLM-optimized documentation sources like llms.txt.
-
-**Recommendation:** Implement in phases, starting with foundation fixes, then catalog system, then active scripts. Test thoroughly with Hono before making it the default for all llms.txt-based skills.
-
----
-
-## References
-
-- Original brainstorming session: 2025-10-24
-- llms.txt convention: https://llmstxt.org/
-- Hono example: https://hono.dev/llms-full.txt
-- Skill_Seekers repository: Current project
-
----
-
-## Appendix: Example Workflows
-
-### Example 1: Agent Searches for "Middleware"
-
-```bash
-# Agent runs:
-python scripts/search.py "middleware"
-
-# Script returns ~8k of middleware documentation from llms-full.md
-# Agent uses that 8k to answer the question
-# Total context used: 8k (not 319k!)
-```
-
-### Example 2: Agent Explores Documentation
-
-```bash
-# 1. Agent lists topics
-python scripts/list_topics.py
-# Returns: Table of contents (2k)
-
-# 2. Agent picks a topic
-python scripts/get_section.py "Routing"
-# Returns: Complete Routing section (5k)
-
-# 3. Agent searches related topics
-python scripts/search.py "path parameters"
-# Returns: Routing + Path section (7k)
-
-# Total context used across 3 queries: 14k (not 3 × 319k = 957k!)
-```
-
-### Example 3: Agent Needs Quick Answer
-
-```bash
-# Agent uses quick variant for overview
-python scripts/search.py "cors" --detail quick
-
-# Returns: Short CORS explanation from llms-small.md (2k)
-# If insufficient, agent can follow up with:
-python scripts/get_section.py "Middleware" # Full section from llms-full.md
-```
-
----
-
-**Document Status:** Ready for review and implementation planning.
diff --git a/assets/repo/Skill_Seekers-development/docs/plans/2025-10-24-active-skills-phase1.md b/assets/repo/Skill_Seekers-development/docs/plans/2025-10-24-active-skills-phase1.md
deleted file mode 100644
index 24eb59f..0000000
--- a/assets/repo/Skill_Seekers-development/docs/plans/2025-10-24-active-skills-phase1.md
+++ /dev/null
@@ -1,682 +0,0 @@
-# Active Skills Phase 1: Foundation Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Fix fundamental issues in llms.txt handling: rename .txt→.md, download all 3 variants, remove truncation.
-
-**Architecture:** Modify existing llms.txt download/parse/build workflow to handle multiple variants correctly, store with proper extensions, and preserve complete content without truncation.
-
-**Tech Stack:** Python 3.10+, requests, BeautifulSoup4, existing Skill_Seekers architecture
-
----
-
-## Task 1: Add Multi-Variant Detection
-
-**Files:**
-- Modify: `cli/llms_txt_detector.py`
-- Test: `tests/test_llms_txt_detector.py`
-
-**Step 1: Write failing test for detect_all() method**
-
-```python
-# tests/test_llms_txt_detector.py (add new test)
-
-def test_detect_all_variants():
- """Test detecting all llms.txt variants"""
- from unittest.mock import patch, Mock
-
- detector = LlmsTxtDetector("https://hono.dev/docs")
-
- with patch('cli.llms_txt_detector.requests.head') as mock_head:
- # Mock responses for different variants
- def mock_response(url, **kwargs):
- response = Mock()
- # All 3 variants exist for Hono
- if 'llms-full.txt' in url or 'llms.txt' in url or 'llms-small.txt' in url:
- response.status_code = 200
- else:
- response.status_code = 404
- return response
-
- mock_head.side_effect = mock_response
-
- variants = detector.detect_all()
-
- assert len(variants) == 3
- assert any(v['variant'] == 'full' for v in variants)
- assert any(v['variant'] == 'standard' for v in variants)
- assert any(v['variant'] == 'small' for v in variants)
- assert all('url' in v for v in variants)
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `source .venv/bin/activate && pytest tests/test_llms_txt_detector.py::test_detect_all_variants -v`
-
-Expected: FAIL with "AttributeError: 'LlmsTxtDetector' object has no attribute 'detect_all'"
-
-**Step 3: Implement detect_all() method**
-
-```python
-# cli/llms_txt_detector.py (add new method)
-
-def detect_all(self) -> List[Dict[str, str]]:
- """
- Detect all available llms.txt variants.
-
- Returns:
- List of dicts with 'url' and 'variant' keys for each found variant
- """
- found_variants = []
-
- for filename, variant in self.VARIANTS:
- parsed = urlparse(self.base_url)
- root_url = f"{parsed.scheme}://{parsed.netloc}"
- url = f"{root_url}/{filename}"
-
- if self._check_url_exists(url):
- found_variants.append({
- 'url': url,
- 'variant': variant
- })
-
- return found_variants
-```
-
-**Step 4: Add import for List and Dict at top of file**
-
-```python
-# cli/llms_txt_detector.py (add to imports)
-from typing import Optional, Dict, List
-```
-
-**Step 5: Run test to verify it passes**
-
-Run: `source .venv/bin/activate && pytest tests/test_llms_txt_detector.py::test_detect_all_variants -v`
-
-Expected: PASS
-
-**Step 6: Commit**
-
-```bash
-git add cli/llms_txt_detector.py tests/test_llms_txt_detector.py
-git commit -m "feat: add detect_all() for multi-variant detection"
-```
-
----
-
-## Task 2: Add File Extension Renaming to Downloader
-
-**Files:**
-- Modify: `cli/llms_txt_downloader.py`
-- Test: `tests/test_llms_txt_downloader.py`
-
-**Step 1: Write failing test for get_proper_filename() method**
-
-```python
-# tests/test_llms_txt_downloader.py (add new test)
-
-def test_get_proper_filename():
- """Test filename conversion from .txt to .md"""
- downloader = LlmsTxtDownloader("https://hono.dev/llms-full.txt")
-
- filename = downloader.get_proper_filename()
-
- assert filename == "llms-full.md"
- assert not filename.endswith('.txt')
-
-def test_get_proper_filename_standard():
- """Test standard variant naming"""
- downloader = LlmsTxtDownloader("https://hono.dev/llms.txt")
-
- filename = downloader.get_proper_filename()
-
- assert filename == "llms.md"
-
-def test_get_proper_filename_small():
- """Test small variant naming"""
- downloader = LlmsTxtDownloader("https://hono.dev/llms-small.txt")
-
- filename = downloader.get_proper_filename()
-
- assert filename == "llms-small.md"
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `source .venv/bin/activate && pytest tests/test_llms_txt_downloader.py::test_get_proper_filename -v`
-
-Expected: FAIL with "AttributeError: 'LlmsTxtDownloader' object has no attribute 'get_proper_filename'"
-
-**Step 3: Implement get_proper_filename() method**
-
-```python
-# cli/llms_txt_downloader.py (add new method)
-
-def get_proper_filename(self) -> str:
- """
- Extract filename from URL and convert .txt to .md
-
- Returns:
- Proper filename with .md extension
-
- Examples:
- https://hono.dev/llms-full.txt -> llms-full.md
- https://hono.dev/llms.txt -> llms.md
- https://hono.dev/llms-small.txt -> llms-small.md
- """
- # Extract filename from URL
- from urllib.parse import urlparse
- parsed = urlparse(self.url)
- filename = parsed.path.split('/')[-1]
-
- # Replace .txt with .md
- if filename.endswith('.txt'):
- filename = filename[:-4] + '.md'
-
- return filename
-```
-
-**Step 4: Run test to verify it passes**
-
-Run: `source .venv/bin/activate && pytest tests/test_llms_txt_downloader.py::test_get_proper_filename -v`
-
-Expected: PASS (all 3 tests)
-
-**Step 5: Commit**
-
-```bash
-git add cli/llms_txt_downloader.py tests/test_llms_txt_downloader.py
-git commit -m "feat: add get_proper_filename() for .txt to .md conversion"
-```
-
----
-
-## Task 3: Update _try_llms_txt() to Download All Variants
-
-**Files:**
-- Modify: `cli/doc_scraper.py:337-384` (_try_llms_txt method)
-- Test: `tests/test_integration.py`
-
-**Step 1: Write failing test for multi-variant download**
-
-```python
-# tests/test_integration.py (add to TestFullLlmsTxtWorkflow class)
-
-def test_multi_variant_download(self):
- """Test downloading all 3 llms.txt variants"""
- from unittest.mock import patch, Mock
- import tempfile
- import os
-
- config = {
- 'name': 'test-multi-variant',
- 'base_url': 'https://hono.dev/docs'
- }
-
- # Mock all 3 variants
- sample_full = "# Full\n" + "x" * 1000
- sample_standard = "# Standard\n" + "x" * 200
- sample_small = "# Small\n" + "x" * 500
-
- with tempfile.TemporaryDirectory() as tmpdir:
- with patch('cli.llms_txt_detector.requests.head') as mock_head, \
- patch('cli.llms_txt_downloader.requests.get') as mock_get:
-
- # Mock detection (all exist)
- mock_head_response = Mock()
- mock_head_response.status_code = 200
- mock_head.return_value = mock_head_response
-
- # Mock downloads
- def mock_download(url, **kwargs):
- response = Mock()
- response.status_code = 200
- if 'llms-full.txt' in url:
- response.text = sample_full
- elif 'llms-small.txt' in url:
- response.text = sample_small
- else: # llms.txt
- response.text = sample_standard
- return response
-
- mock_get.side_effect = mock_download
-
- # Run scraper
- scraper = DocumentationScraper(config, dry_run=False)
- result = scraper._try_llms_txt()
-
- # Verify all 3 files created
- refs_dir = os.path.join(scraper.skill_dir, 'references')
-
- assert os.path.exists(os.path.join(refs_dir, 'llms-full.md'))
- assert os.path.exists(os.path.join(refs_dir, 'llms.md'))
- assert os.path.exists(os.path.join(refs_dir, 'llms-small.md'))
-
- # Verify content not truncated
- with open(os.path.join(refs_dir, 'llms-full.md')) as f:
- content = f.read()
- assert len(content) == len(sample_full)
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `source .venv/bin/activate && pytest tests/test_integration.py::TestFullLlmsTxtWorkflow::test_multi_variant_download -v`
-
-Expected: FAIL - only one file created, not all 3
-
-**Step 3: Modify _try_llms_txt() to use detect_all()**
-
-```python
-# cli/doc_scraper.py (replace _try_llms_txt method, lines 337-384)
-
-def _try_llms_txt(self) -> bool:
- """
- Try to use llms.txt instead of HTML scraping.
- Downloads ALL available variants and stores with .md extension.
-
- Returns:
- True if llms.txt was found and processed successfully
- """
- print(f"\n🔍 Checking for llms.txt at {self.base_url}...")
-
- # Check for explicit config URL first
- explicit_url = self.config.get('llms_txt_url')
- if explicit_url:
- print(f"\n📌 Using explicit llms_txt_url from config: {explicit_url}")
-
- downloader = LlmsTxtDownloader(explicit_url)
- content = downloader.download()
-
- if content:
- # Save with proper .md extension
- filename = downloader.get_proper_filename()
- filepath = os.path.join(self.skill_dir, "references", filename)
- os.makedirs(os.path.dirname(filepath), exist_ok=True)
-
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(content)
- print(f" 💾 Saved {filename} ({len(content)} chars)")
-
- # Parse and save pages
- parser = LlmsTxtParser(content)
- pages = parser.parse()
-
- if pages:
- for page in pages:
- self.save_page(page)
- self.pages.append(page)
-
- self.llms_txt_detected = True
- self.llms_txt_variant = 'explicit'
- return True
-
- # Auto-detection: Find ALL variants
- detector = LlmsTxtDetector(self.base_url)
- variants = detector.detect_all()
-
- if not variants:
- print("ℹ️ No llms.txt found, using HTML scraping")
- return False
-
- print(f"✅ Found {len(variants)} llms.txt variant(s)")
-
- # Download ALL variants
- downloaded = {}
- for variant_info in variants:
- url = variant_info['url']
- variant = variant_info['variant']
-
- print(f" 📥 Downloading {variant}...")
- downloader = LlmsTxtDownloader(url)
- content = downloader.download()
-
- if content:
- filename = downloader.get_proper_filename()
- downloaded[variant] = {
- 'content': content,
- 'filename': filename,
- 'size': len(content)
- }
- print(f" ✓ {filename} ({len(content)} chars)")
-
- if not downloaded:
- print("⚠️ Failed to download any variants, falling back to HTML scraping")
- return False
-
- # Save ALL variants to references/
- os.makedirs(os.path.join(self.skill_dir, "references"), exist_ok=True)
-
- for variant, data in downloaded.items():
- filepath = os.path.join(self.skill_dir, "references", data['filename'])
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(data['content'])
- print(f" 💾 Saved {data['filename']}")
-
- # Parse LARGEST variant for skill building
- largest = max(downloaded.items(), key=lambda x: x[1]['size'])
- print(f"\n📄 Parsing {largest[1]['filename']} for skill building...")
-
- parser = LlmsTxtParser(largest[1]['content'])
- pages = parser.parse()
-
- if not pages:
- print("⚠️ Failed to parse llms.txt, falling back to HTML scraping")
- return False
-
- print(f" ✓ Parsed {len(pages)} sections")
-
- # Save pages for skill building
- for page in pages:
- self.save_page(page)
- self.pages.append(page)
-
- self.llms_txt_detected = True
- self.llms_txt_variants = list(downloaded.keys())
-
- return True
-```
-
-**Step 4: Add llms_txt_variants attribute to __init__**
-
-```python
-# cli/doc_scraper.py (in __init__ method, after llms_txt_variant line)
-
-self.llms_txt_variants = [] # Track all downloaded variants
-```
-
-**Step 5: Run test to verify it passes**
-
-Run: `source .venv/bin/activate && pytest tests/test_integration.py::TestFullLlmsTxtWorkflow::test_multi_variant_download -v`
-
-Expected: PASS
-
-**Step 6: Commit**
-
-```bash
-git add cli/doc_scraper.py tests/test_integration.py
-git commit -m "feat: download all llms.txt variants with proper .md extension"
-```
-
----
-
-## Task 4: Remove Content Truncation
-
-**Files:**
-- Modify: `cli/doc_scraper.py:714-730` (create_reference_file method)
-
-**Step 1: Write failing test for no truncation**
-
-```python
-# tests/test_integration.py (add new test)
-
-def test_no_content_truncation():
- """Test that content is NOT truncated in reference files"""
- from unittest.mock import Mock
- import tempfile
- import os
-
- config = {
- 'name': 'test-no-truncate',
- 'base_url': 'https://example.com/docs'
- }
-
- # Create scraper with long content
- scraper = DocumentationScraper(config, dry_run=False)
-
- # Create page with content > 2500 chars
- long_content = "x" * 5000
- long_code = "y" * 1000
-
- pages = [{
- 'title': 'Long Page',
- 'url': 'https://example.com/long',
- 'content': long_content,
- 'code_samples': [
- {'code': long_code, 'language': 'python'}
- ],
- 'headings': []
- }]
-
- # Create reference file
- scraper.create_reference_file('test', pages)
-
- # Verify no truncation
- ref_file = os.path.join(scraper.skill_dir, 'references', 'test.md')
- with open(ref_file, 'r') as f:
- content = f.read()
-
- assert long_content in content # Full content included
- assert long_code in content # Full code included
- assert '[Content truncated]' not in content
- assert '...' not in content or content.count('...') == 0
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `source .venv/bin/activate && pytest tests/test_integration.py::test_no_content_truncation -v`
-
-Expected: FAIL - content contains "[Content truncated]" or "..."
-
-**Step 3: Remove truncation from create_reference_file()**
-
-```python
-# cli/doc_scraper.py (modify create_reference_file method, lines 712-731)
-
-# OLD (line 714-716):
-# if page.get('content'):
-# content = page['content'][:2500]
-# if len(page['content']) > 2500:
-# content += "\n\n*[Content truncated]*"
-
-# NEW (replace with):
- if page.get('content'):
- content = page['content'] # NO TRUNCATION
- lines.append(content)
- lines.append("")
-
-# OLD (line 728-730):
-# lines.append(code[:600])
-# if len(code) > 600:
-# lines.append("...")
-
-# NEW (replace with):
- lines.append(code) # NO TRUNCATION
- # No "..." suffix
-```
-
-**Complete replacement of lines 712-731:**
-
-```python
-# cli/doc_scraper.py:712-731 (complete replacement)
-
- # Content (NO TRUNCATION)
- if page.get('content'):
- lines.append(page['content'])
- lines.append("")
-
- # Code examples with language (NO TRUNCATION)
- if page.get('code_samples'):
- lines.append("**Examples:**\n")
- for i, sample in enumerate(page['code_samples'][:4], 1):
- lang = sample.get('language', 'unknown')
- code = sample.get('code', sample if isinstance(sample, str) else '')
- lines.append(f"Example {i} ({lang}):")
- lines.append(f"```{lang}")
- lines.append(code) # Full code, no truncation
- lines.append("```\n")
-```
-
-**Step 4: Run test to verify it passes**
-
-Run: `source .venv/bin/activate && pytest tests/test_integration.py::test_no_content_truncation -v`
-
-Expected: PASS
-
-**Step 5: Run full test suite to check for regressions**
-
-Run: `source .venv/bin/activate && pytest tests/ -v`
-
-Expected: All 201+ tests pass
-
-**Step 6: Commit**
-
-```bash
-git add cli/doc_scraper.py tests/test_integration.py
-git commit -m "feat: remove content truncation in reference files"
-```
-
----
-
-## Task 5: Update Documentation
-
-**Files:**
-- Modify: `docs/plans/2025-10-24-active-skills-design.md`
-- Modify: `CHANGELOG.md`
-
-**Step 1: Update design doc status**
-
-```markdown
-# docs/plans/2025-10-24-active-skills-design.md (update header)
-
-**Status:** Phase 1 Implemented ✅
-```
-
-**Step 2: Add CHANGELOG entry**
-
-```markdown
-# CHANGELOG.md (add new section at top)
-
-## [Unreleased]
-
-### Added - Phase 1: Active Skills Foundation
-- Multi-variant llms.txt detection: downloads all 3 variants (full, standard, small)
-- Automatic .txt → .md file extension conversion
-- No content truncation: preserves complete documentation
-- `detect_all()` method for finding all llms.txt variants
-- `get_proper_filename()` for correct .md naming
-
-### Changed
-- `_try_llms_txt()` now downloads all available variants instead of just one
-- Reference files now contain complete content (no 2500 char limit)
-- Code samples now include full code (no 600 char limit)
-
-### Fixed
-- File extension bug: llms.txt files now saved as .md
-- Content loss: 0% truncation (was 36%)
-```
-
-**Step 3: Commit**
-
-```bash
-git add docs/plans/2025-10-24-active-skills-design.md CHANGELOG.md
-git commit -m "docs: update status for Phase 1 completion"
-```
-
----
-
-## Task 6: Manual Verification
-
-**Files:**
-- None (manual testing)
-
-**Step 1: Test with Hono config**
-
-Run: `source .venv/bin/activate && python3 cli/doc_scraper.py --config configs/hono.json`
-
-**Expected output:**
-```
-🔍 Checking for llms.txt at https://hono.dev/docs...
-📌 Using explicit llms_txt_url from config: https://hono.dev/llms-full.txt
- 💾 Saved llms-full.md (319000 chars)
-📄 Parsing llms-full.md for skill building...
- ✓ Parsed 93 sections
-✅ Used llms.txt (explicit) - skipping HTML scraping
-```
-
-**Step 2: Verify all 3 files exist with correct extensions**
-
-Run: `ls -lah output/hono/references/llms*.md`
-
-Expected:
-```
-llms-full.md 319k
-llms.md 5.4k
-llms-small.md 176k
-```
-
-**Step 3: Verify no truncation in reference files**
-
-Run: `grep -c "Content truncated" output/hono/references/*.md`
-
-Expected: 0 matches (no truncation messages)
-
-**Step 4: Check file sizes are correct**
-
-Run: `wc -c output/hono/references/llms-full.md`
-
-Expected: Should match original download size (~319k), not reduced to 203k
-
-**Step 5: Verify all tests still pass**
-
-Run: `source .venv/bin/activate && pytest tests/ -v`
-
-Expected: All tests pass (201+)
-
----
-
-## Completion Checklist
-
-- [ ] Task 1: Multi-variant detection (detect_all)
-- [ ] Task 2: File extension renaming (get_proper_filename)
-- [ ] Task 3: Download all variants (_try_llms_txt)
-- [ ] Task 4: Remove truncation (create_reference_file)
-- [ ] Task 5: Update documentation
-- [ ] Task 6: Manual verification
-- [ ] All tests passing
-- [ ] No regressions in existing functionality
-
----
-
-## Success Criteria
-
-**Technical:**
-- ✅ All 3 variants downloaded when available
-- ✅ Files saved with .md extension (not .txt)
-- ✅ 0% content truncation (was 36%)
-- ✅ All existing tests pass
-- ✅ New tests cover all changes
-
-**User Experience:**
-- ✅ Hono skill has all 3 files: llms-full.md, llms.md, llms-small.md
-- ✅ Reference files contain complete documentation
-- ✅ No "[Content truncated]" messages in output
-
----
-
-## Related Skills
-
-- @superpowers:test-driven-development - Used throughout for TDD approach
-- @superpowers:verification-before-completion - Used in Task 6 for manual verification
-
----
-
-## Notes
-
-- This plan implements Phase 1 from `docs/plans/2025-10-24-active-skills-design.md`
-- Phase 2 (Catalog System) and Phase 3 (Active Scripts) will be separate plans
-- All changes maintain backward compatibility with existing HTML scraping
-- File extension fix (.txt → .md) is critical for proper skill functionality
-
----
-
-## Estimated Time
-
-- Task 1: 15 minutes
-- Task 2: 15 minutes
-- Task 3: 30 minutes
-- Task 4: 20 minutes
-- Task 5: 10 minutes
-- Task 6: 15 minutes
-
-**Total: ~1.5 hours**
diff --git a/assets/repo/Skill_Seekers-development/example-mcp-config.json b/assets/repo/Skill_Seekers-development/example-mcp-config.json
deleted file mode 100644
index 80d946c..0000000
--- a/assets/repo/Skill_Seekers-development/example-mcp-config.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "python3",
- "args": [
- "/mnt/1ece809a-2821-4f10-aecb-fcdf34760c0b/Git/Skill_Seekers/mcp/server.py"
- ],
- "cwd": "/mnt/1ece809a-2821-4f10-aecb-fcdf34760c0b/Git/Skill_Seekers"
- }
- }
-}
diff --git a/assets/repo/Skill_Seekers-development/mypy.ini b/assets/repo/Skill_Seekers-development/mypy.ini
deleted file mode 100644
index 857c31c..0000000
--- a/assets/repo/Skill_Seekers-development/mypy.ini
+++ /dev/null
@@ -1,13 +0,0 @@
-[mypy]
-python_version = 3.10
-warn_return_any = False
-warn_unused_configs = True
-disallow_untyped_defs = False
-check_untyped_defs = True
-ignore_missing_imports = True
-no_implicit_optional = True
-show_error_codes = True
-
-# Gradual typing - be lenient for now
-disallow_incomplete_defs = False
-disallow_untyped_calls = False
diff --git a/assets/repo/Skill_Seekers-development/pyproject.toml b/assets/repo/Skill_Seekers-development/pyproject.toml
deleted file mode 100644
index 91c8391..0000000
--- a/assets/repo/Skill_Seekers-development/pyproject.toml
+++ /dev/null
@@ -1,149 +0,0 @@
-[build-system]
-requires = ["setuptools>=61.0", "wheel"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "skill-seekers"
-version = "2.1.1"
-description = "Convert documentation websites, GitHub repositories, and PDFs into Claude AI skills"
-readme = "README.md"
-requires-python = ">=3.10"
-license = {text = "MIT"}
-authors = [
- {name = "Yusuf Karaaslan"}
-]
-keywords = [
- "claude",
- "ai",
- "documentation",
- "scraping",
- "skills",
- "llm",
- "mcp",
- "automation"
-]
-classifiers = [
- "Development Status :: 4 - Beta",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
- "Operating System :: OS Independent",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
- "Topic :: Software Development :: Documentation",
- "Topic :: Software Development :: Libraries :: Python Modules",
- "Topic :: Text Processing :: Markup :: Markdown",
-]
-
-# Core dependencies
-dependencies = [
- "requests>=2.32.5",
- "beautifulsoup4>=4.14.2",
- "PyGithub>=2.5.0",
- "mcp>=1.18.0",
- "httpx>=0.28.1",
- "httpx-sse>=0.4.3",
- "PyMuPDF>=1.24.14",
- "Pillow>=11.0.0",
- "pytesseract>=0.3.13",
- "pydantic>=2.12.3",
- "pydantic-settings>=2.11.0",
- "python-dotenv>=1.1.1",
- "jsonschema>=4.25.1",
- "click>=8.3.0",
- "Pygments>=2.19.2",
-]
-
-[project.optional-dependencies]
-# Development dependencies
-dev = [
- "pytest>=8.4.2",
- "pytest-cov>=7.0.0",
- "coverage>=7.11.0",
-]
-
-# MCP server dependencies (included by default, but optional)
-mcp = [
- "mcp>=1.18.0",
- "httpx>=0.28.1",
- "httpx-sse>=0.4.3",
- "uvicorn>=0.38.0",
- "starlette>=0.48.0",
- "sse-starlette>=3.0.2",
-]
-
-# All optional dependencies combined
-all = [
- "pytest>=8.4.2",
- "pytest-cov>=7.0.0",
- "coverage>=7.11.0",
- "mcp>=1.18.0",
- "httpx>=0.28.1",
- "httpx-sse>=0.4.3",
- "uvicorn>=0.38.0",
- "starlette>=0.48.0",
- "sse-starlette>=3.0.2",
-]
-
-[project.urls]
-Homepage = "https://github.com/yusufkaraaslan/Skill_Seekers"
-Repository = "https://github.com/yusufkaraaslan/Skill_Seekers"
-"Bug Tracker" = "https://github.com/yusufkaraaslan/Skill_Seekers/issues"
-Documentation = "https://github.com/yusufkaraaslan/Skill_Seekers#readme"
-
-[project.scripts]
-# Main unified CLI
-skill-seekers = "skill_seekers.cli.main:main"
-
-# Individual tool entry points
-skill-seekers-scrape = "skill_seekers.cli.doc_scraper:main"
-skill-seekers-github = "skill_seekers.cli.github_scraper:main"
-skill-seekers-pdf = "skill_seekers.cli.pdf_scraper:main"
-skill-seekers-unified = "skill_seekers.cli.unified_scraper:main"
-skill-seekers-enhance = "skill_seekers.cli.enhance_skill_local:main"
-skill-seekers-package = "skill_seekers.cli.package_skill:main"
-skill-seekers-upload = "skill_seekers.cli.upload_skill:main"
-skill-seekers-estimate = "skill_seekers.cli.estimate_pages:main"
-
-[tool.setuptools]
-packages = ["skill_seekers", "skill_seekers.cli", "skill_seekers.mcp", "skill_seekers.mcp.tools"]
-
-[tool.setuptools.package-dir]
-"" = "src"
-
-[tool.setuptools.package-data]
-skill_seekers = ["py.typed"]
-
-[tool.pytest.ini_options]
-testpaths = ["tests"]
-python_files = ["test_*.py"]
-python_classes = ["Test*"]
-python_functions = ["test_*"]
-addopts = "-v --tb=short --strict-markers"
-
-[tool.coverage.run]
-source = ["src/skill_seekers"]
-omit = ["*/tests/*", "*/__pycache__/*", "*/venv/*"]
-
-[tool.coverage.report]
-exclude_lines = [
- "pragma: no cover",
- "def __repr__",
- "raise AssertionError",
- "raise NotImplementedError",
- "if __name__ == .__main__.:",
- "if TYPE_CHECKING:",
- "@abstractmethod",
-]
-
-[tool.uv]
-dev-dependencies = [
- "pytest>=8.4.2",
- "pytest-cov>=7.0.0",
- "coverage>=7.11.0",
-]
-
-[tool.uv.sources]
-# Use PyPI for all dependencies
diff --git a/assets/repo/Skill_Seekers-development/requirements.txt b/assets/repo/Skill_Seekers-development/requirements.txt
deleted file mode 100644
index c6e9ced..0000000
--- a/assets/repo/Skill_Seekers-development/requirements.txt
+++ /dev/null
@@ -1,42 +0,0 @@
-annotated-types==0.7.0
-anyio==4.11.0
-attrs==25.4.0
-beautifulsoup4==4.14.2
-certifi==2025.10.5
-charset-normalizer==3.4.4
-click==8.3.0
-coverage==7.11.0
-h11==0.16.0
-httpcore==1.0.9
-httpx==0.28.1
-httpx-sse==0.4.3
-idna==3.11
-iniconfig==2.3.0
-jsonschema==4.25.1
-jsonschema-specifications==2025.9.1
-mcp==1.18.0
-packaging==25.0
-pluggy==1.6.0
-pydantic==2.12.3
-pydantic-settings==2.11.0
-pydantic_core==2.41.4
-PyGithub==2.5.0
-Pygments==2.19.2
-PyMuPDF==1.24.14
-Pillow==11.0.0
-pytesseract==0.3.13
-pytest==8.4.2
-pytest-cov==7.0.0
-python-dotenv==1.1.1
-python-multipart==0.0.20
-referencing==0.37.0
-requests==2.32.5
-rpds-py==0.27.1
-sniffio==1.3.1
-soupsieve==2.8
-sse-starlette==3.0.2
-starlette==0.48.0
-typing-inspection==0.4.2
-typing_extensions==4.15.0
-urllib3==2.5.0
-uvicorn==0.38.0
diff --git a/assets/repo/Skill_Seekers-development/setup_mcp.sh b/assets/repo/Skill_Seekers-development/setup_mcp.sh
deleted file mode 100644
index 4047102..0000000
--- a/assets/repo/Skill_Seekers-development/setup_mcp.sh
+++ /dev/null
@@ -1,266 +0,0 @@
-#!/bin/bash
-# Skill Seeker MCP Server - Quick Setup Script
-# This script automates the MCP server setup for Claude Code
-
-set -e # Exit on error
-
-echo "=================================================="
-echo "Skill Seeker MCP Server - Quick Setup"
-echo "=================================================="
-echo ""
-
-# Colors for output
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-RED='\033[0;31m'
-NC='\033[0m' # No Color
-
-# Step 1: Check Python version
-echo "Step 1: Checking Python version..."
-if ! command -v python3 &> /dev/null; then
- echo -e "${RED}❌ Error: python3 not found${NC}"
- echo "Please install Python 3.7 or higher"
- exit 1
-fi
-
-PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
-echo -e "${GREEN}✓${NC} Python $PYTHON_VERSION found"
-echo ""
-
-# Step 2: Get repository path
-REPO_PATH=$(pwd)
-echo "Step 2: Repository location"
-echo "Path: $REPO_PATH"
-echo ""
-
-# Step 3: Install dependencies
-echo "Step 3: Installing Python dependencies..."
-
-# Check if we're in a virtual environment
-if [[ -n "$VIRTUAL_ENV" ]]; then
- echo -e "${GREEN}✓${NC} Virtual environment detected: $VIRTUAL_ENV"
- PIP_INSTALL_CMD="pip install"
-elif [[ -d "venv" ]]; then
- echo -e "${YELLOW}⚠${NC} Virtual environment found but not activated"
- echo "Activating venv..."
- source venv/bin/activate
- PIP_INSTALL_CMD="pip install"
-else
- echo -e "${YELLOW}⚠${NC} No virtual environment found"
- echo "It's recommended to use a virtual environment to avoid conflicts."
- echo ""
- read -p "Would you like to create one now? (y/n) " -n 1 -r
- echo ""
-
- if [[ $REPLY =~ ^[Yy]$ ]]; then
- echo "Creating virtual environment..."
- python3 -m venv venv || {
- echo -e "${RED}❌ Failed to create virtual environment${NC}"
- echo "Falling back to system install..."
- PIP_INSTALL_CMD="pip3 install --user --break-system-packages"
- }
-
- if [[ -d "venv" ]]; then
- source venv/bin/activate
- PIP_INSTALL_CMD="pip install"
- echo -e "${GREEN}✓${NC} Virtual environment created and activated"
- fi
- else
- echo "Proceeding with system install (using --user --break-system-packages)..."
- echo -e "${YELLOW}Note:${NC} This may override system-managed packages"
- PIP_INSTALL_CMD="pip3 install --user --break-system-packages"
- fi
-fi
-
-echo "This will install: mcp, requests, beautifulsoup4"
-read -p "Continue? (y/n) " -n 1 -r
-echo ""
-
-if [[ $REPLY =~ ^[Yy]$ ]]; then
- echo "Installing package in editable mode..."
- $PIP_INSTALL_CMD -e . || {
- echo -e "${RED}❌ Failed to install package${NC}"
- exit 1
- }
-
- echo -e "${GREEN}✓${NC} Dependencies installed successfully"
-else
- echo "Skipping dependency installation"
-fi
-echo ""
-
-# Step 4: Test MCP server
-echo "Step 4: Testing MCP server..."
-timeout 3 python3 src/skill_seekers/mcp/server.py 2>/dev/null || {
- if [ $? -eq 124 ]; then
- echo -e "${GREEN}✓${NC} MCP server starts correctly (timeout expected)"
- else
- echo -e "${YELLOW}⚠${NC} MCP server test inconclusive, but may still work"
- fi
-}
-echo ""
-
-# Step 5: Optional - Run tests
-echo "Step 5: Run test suite? (optional)"
-read -p "Run MCP tests to verify everything works? (y/n) " -n 1 -r
-echo ""
-
-if [[ $REPLY =~ ^[Yy]$ ]]; then
- # Check if pytest is installed
- if ! command -v pytest &> /dev/null; then
- echo "Installing pytest..."
- $PIP_INSTALL_CMD pytest || {
- echo -e "${YELLOW}⚠${NC} Could not install pytest, skipping tests"
- }
- fi
-
- if command -v pytest &> /dev/null; then
- echo "Running MCP server tests..."
- python3 -m pytest tests/test_mcp_server.py -v --tb=short || {
- echo -e "${RED}❌ Some tests failed${NC}"
- echo "The server may still work, but please check the errors above"
- }
- fi
-else
- echo "Skipping tests"
-fi
-echo ""
-
-# Step 6: Configure Claude Code
-echo "Step 6: Configure Claude Code"
-echo "=================================================="
-echo ""
-echo "You need to add this configuration to Claude Code:"
-echo ""
-echo -e "${YELLOW}Configuration file:${NC} ~/.config/claude-code/mcp.json"
-echo ""
-echo "Add this JSON configuration (paths are auto-detected for YOUR system):"
-echo ""
-echo -e "${GREEN}{"
-echo " \"mcpServers\": {"
-echo " \"skill-seeker\": {"
-echo " \"command\": \"python3\","
-echo " \"args\": ["
-echo " \"$REPO_PATH/src/skill_seekers/mcp/server.py\""
-echo " ],"
-echo " \"cwd\": \"$REPO_PATH\""
-echo " }"
-echo " }"
-echo -e "}${NC}"
-echo ""
-echo -e "${YELLOW}Note:${NC} The paths above are YOUR actual paths (not placeholders!)"
-echo ""
-
-# Ask if user wants auto-configure
-echo ""
-read -p "Auto-configure Claude Code now? (y/n) " -n 1 -r
-echo ""
-
-if [[ $REPLY =~ ^[Yy]$ ]]; then
- # Check if config already exists
- if [ -f ~/.config/claude-code/mcp.json ]; then
- echo -e "${YELLOW}⚠ Warning: ~/.config/claude-code/mcp.json already exists${NC}"
- echo "Current contents:"
- cat ~/.config/claude-code/mcp.json
- echo ""
- read -p "Overwrite? (y/n) " -n 1 -r
- echo ""
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
- echo "Skipping auto-configuration"
- echo "Please manually add the skill-seeker server to your config"
- exit 0
- fi
- fi
-
- # Create config directory
- mkdir -p ~/.config/claude-code
-
- # Write configuration with actual expanded path
- cat > ~/.config/claude-code/mcp.json << EOF
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "python3",
- "args": [
- "$REPO_PATH/src/skill_seekers/mcp/server.py"
- ],
- "cwd": "$REPO_PATH"
- }
- }
-}
-EOF
-
- echo -e "${GREEN}✓${NC} Configuration written to ~/.config/claude-code/mcp.json"
- echo ""
- echo "Configuration contents:"
- cat ~/.config/claude-code/mcp.json
- echo ""
-
- # Verify the path exists
- if [ -f "$REPO_PATH/src/skill_seekers/mcp/server.py" ]; then
- echo -e "${GREEN}✓${NC} Verified: MCP server file exists at $REPO_PATH/src/skill_seekers/mcp/server.py"
- else
- echo -e "${RED}❌ Warning: MCP server not found at $REPO_PATH/src/skill_seekers/mcp/server.py${NC}"
- echo "Please check the path!"
- fi
-else
- echo "Skipping auto-configuration"
- echo "Please manually configure Claude Code using the JSON above"
- echo ""
- echo "IMPORTANT: Replace \$REPO_PATH with the actual path: $REPO_PATH"
-fi
-echo ""
-
-# Step 7: Test the configuration
-if [ -f ~/.config/claude-code/mcp.json ]; then
- echo "Step 7: Testing MCP configuration..."
- echo "Checking if paths are correct..."
-
- # Extract the configured path
- if command -v jq &> /dev/null; then
- CONFIGURED_PATH=$(jq -r '.mcpServers["skill-seeker"].args[0]' ~/.config/claude-code/mcp.json 2>/dev/null || echo "")
- if [ -n "$CONFIGURED_PATH" ] && [ -f "$CONFIGURED_PATH" ]; then
- echo -e "${GREEN}✓${NC} MCP server path is valid: $CONFIGURED_PATH"
- elif [ -n "$CONFIGURED_PATH" ]; then
- echo -e "${YELLOW}⚠${NC} Warning: Configured path doesn't exist: $CONFIGURED_PATH"
- fi
- else
- echo "Install 'jq' for config validation: brew install jq (macOS) or apt install jq (Linux)"
- fi
-fi
-echo ""
-
-# Step 8: Final instructions
-echo "=================================================="
-echo "Setup Complete!"
-echo "=================================================="
-echo ""
-echo "Next steps:"
-echo ""
-echo " 1. ${YELLOW}Restart Claude Code${NC} (quit and reopen, don't just close window)"
-echo " 2. In Claude Code, test with: ${GREEN}\"List all available configs\"${NC}"
-echo " 3. You should see 9 Skill Seeker tools available"
-echo ""
-echo "Available MCP Tools:"
-echo " • generate_config - Create new config files"
-echo " • estimate_pages - Estimate scraping time"
-echo " • scrape_docs - Scrape documentation"
-echo " • package_skill - Create .zip files"
-echo " • list_configs - Show available configs"
-echo " • validate_config - Validate config files"
-echo ""
-echo "Example commands to try in Claude Code:"
-echo " • ${GREEN}List all available configs${NC}"
-echo " • ${GREEN}Validate configs/react.json${NC}"
-echo " • ${GREEN}Generate config for Tailwind at https://tailwindcss.com/docs${NC}"
-echo ""
-echo "Documentation:"
-echo " • MCP Setup Guide: ${YELLOW}docs/MCP_SETUP.md${NC}"
-echo " • Full docs: ${YELLOW}README.md${NC}"
-echo ""
-echo "Troubleshooting:"
-echo " • Check logs: ~/Library/Logs/Claude Code/ (macOS)"
-echo " • Test server: python3 src/skill_seekers/mcp/server.py"
-echo " • Run tests: python3 -m pytest tests/test_mcp_server.py -v"
-echo ""
-echo "Happy skill creating! 🚀"
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/__init__.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/__init__.py
deleted file mode 100644
index 752904b..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/__init__.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""
-Skill Seekers - Convert documentation, GitHub repos, and PDFs into Claude AI skills.
-
-This package provides tools for automatically scraping, organizing, and packaging
-documentation from various sources into uploadable Claude AI skills.
-"""
-
-__version__ = "2.0.0"
-__author__ = "Yusuf Karaaslan"
-__license__ = "MIT"
-
-# Expose main components for easier imports
-from skill_seekers.cli import __version__ as cli_version
-from skill_seekers.mcp import __version__ as mcp_version
-
-__all__ = [
- "__version__",
- "__author__",
- "__license__",
- "cli_version",
- "mcp_version",
-]
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/__init__.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/__init__.py
deleted file mode 100644
index d782d5d..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/__init__.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""Skill Seekers CLI tools package.
-
-This package provides command-line tools for converting documentation
-websites into Claude AI skills.
-
-Main modules:
- - doc_scraper: Main documentation scraping and skill building tool
- - llms_txt_detector: Detect llms.txt files at documentation URLs
- - llms_txt_downloader: Download llms.txt content
- - llms_txt_parser: Parse llms.txt markdown content
- - pdf_scraper: Extract documentation from PDF files
- - enhance_skill: AI-powered skill enhancement (API-based)
- - enhance_skill_local: AI-powered skill enhancement (local)
- - estimate_pages: Estimate page count before scraping
- - package_skill: Package skills into .zip files
- - upload_skill: Upload skills to Claude
- - utils: Shared utility functions
-"""
-
-from .llms_txt_detector import LlmsTxtDetector
-from .llms_txt_downloader import LlmsTxtDownloader
-from .llms_txt_parser import LlmsTxtParser
-
-try:
- from .utils import open_folder, read_reference_files
-except ImportError:
- # utils.py might not exist in all configurations
- open_folder = None
- read_reference_files = None
-
-__version__ = "2.0.0"
-
-__all__ = [
- "LlmsTxtDetector",
- "LlmsTxtDownloader",
- "LlmsTxtParser",
- "open_folder",
- "read_reference_files",
-]
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/code_analyzer.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/code_analyzer.py
deleted file mode 100644
index cf33b16..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/code_analyzer.py
+++ /dev/null
@@ -1,500 +0,0 @@
-#!/usr/bin/env python3
-"""
-Code Analyzer for GitHub Repositories
-
-Extracts code signatures at configurable depth levels:
-- surface: File tree only (existing behavior)
-- deep: Parse files for signatures, parameters, types
-- full: Complete AST analysis (future enhancement)
-
-Supports multiple languages with language-specific parsers.
-"""
-
-import ast
-import re
-import logging
-from typing import Dict, List, Any, Optional
-from dataclasses import dataclass, asdict
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class Parameter:
- """Represents a function parameter."""
- name: str
- type_hint: Optional[str] = None
- default: Optional[str] = None
-
-
-@dataclass
-class FunctionSignature:
- """Represents a function/method signature."""
- name: str
- parameters: List[Parameter]
- return_type: Optional[str] = None
- docstring: Optional[str] = None
- line_number: Optional[int] = None
- is_async: bool = False
- is_method: bool = False
- decorators: List[str] = None
-
- def __post_init__(self):
- if self.decorators is None:
- self.decorators = []
-
-
-@dataclass
-class ClassSignature:
- """Represents a class signature."""
- name: str
- base_classes: List[str]
- methods: List[FunctionSignature]
- docstring: Optional[str] = None
- line_number: Optional[int] = None
-
-
-class CodeAnalyzer:
- """
- Analyzes code at different depth levels.
- """
-
- def __init__(self, depth: str = 'surface'):
- """
- Initialize code analyzer.
-
- Args:
- depth: Analysis depth ('surface', 'deep', 'full')
- """
- self.depth = depth
-
- def analyze_file(self, file_path: str, content: str, language: str) -> Dict[str, Any]:
- """
- Analyze a single file based on depth level.
-
- Args:
- file_path: Path to file in repository
- content: File content as string
- language: Programming language (Python, JavaScript, etc.)
-
- Returns:
- Dict containing extracted signatures
- """
- if self.depth == 'surface':
- return {} # Surface level doesn't analyze individual files
-
- logger.debug(f"Analyzing {file_path} (language: {language}, depth: {self.depth})")
-
- try:
- if language == 'Python':
- return self._analyze_python(content, file_path)
- elif language in ['JavaScript', 'TypeScript']:
- return self._analyze_javascript(content, file_path)
- elif language in ['C', 'C++']:
- return self._analyze_cpp(content, file_path)
- else:
- logger.debug(f"No analyzer for language: {language}")
- return {}
- except Exception as e:
- logger.warning(f"Error analyzing {file_path}: {e}")
- return {}
-
- def _analyze_python(self, content: str, file_path: str) -> Dict[str, Any]:
- """Analyze Python file using AST."""
- try:
- tree = ast.parse(content)
- except SyntaxError as e:
- logger.debug(f"Syntax error in {file_path}: {e}")
- return {}
-
- classes = []
- functions = []
-
- for node in ast.walk(tree):
- if isinstance(node, ast.ClassDef):
- class_sig = self._extract_python_class(node)
- classes.append(asdict(class_sig))
- elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
- # Only top-level functions (not methods)
- # Fix AST parser to check isinstance(parent.body, list) before 'in' operator
- is_method = False
- try:
- is_method = any(isinstance(parent, ast.ClassDef)
- for parent in ast.walk(tree)
- if hasattr(parent, 'body') and isinstance(parent.body, list) and node in parent.body)
- except (TypeError, AttributeError):
- # If body is not iterable or check fails, assume it's a top-level function
- is_method = False
-
- if not is_method:
- func_sig = self._extract_python_function(node)
- functions.append(asdict(func_sig))
-
- return {
- 'classes': classes,
- 'functions': functions
- }
-
- def _extract_python_class(self, node: ast.ClassDef) -> ClassSignature:
- """Extract class signature from AST node."""
- # Extract base classes
- bases = []
- for base in node.bases:
- if isinstance(base, ast.Name):
- bases.append(base.id)
- elif isinstance(base, ast.Attribute):
- bases.append(f"{base.value.id}.{base.attr}" if hasattr(base.value, 'id') else base.attr)
-
- # Extract methods
- methods = []
- for item in node.body:
- if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
- method_sig = self._extract_python_function(item, is_method=True)
- methods.append(method_sig)
-
- # Extract docstring
- docstring = ast.get_docstring(node)
-
- return ClassSignature(
- name=node.name,
- base_classes=bases,
- methods=methods,
- docstring=docstring,
- line_number=node.lineno
- )
-
- def _extract_python_function(self, node, is_method: bool = False) -> FunctionSignature:
- """Extract function signature from AST node."""
- # Extract parameters
- params = []
- for arg in node.args.args:
- param_type = None
- if arg.annotation:
- param_type = ast.unparse(arg.annotation) if hasattr(ast, 'unparse') else None
-
- params.append(Parameter(
- name=arg.arg,
- type_hint=param_type
- ))
-
- # Extract defaults
- defaults = node.args.defaults
- if defaults:
- # Defaults are aligned to the end of params
- num_no_default = len(params) - len(defaults)
- for i, default in enumerate(defaults):
- param_idx = num_no_default + i
- if param_idx < len(params):
- try:
- params[param_idx].default = ast.unparse(default) if hasattr(ast, 'unparse') else str(default)
- except:
- params[param_idx].default = "..."
-
- # Extract return type
- return_type = None
- if node.returns:
- try:
- return_type = ast.unparse(node.returns) if hasattr(ast, 'unparse') else None
- except:
- pass
-
- # Extract decorators
- decorators = []
- for decorator in node.decorator_list:
- try:
- if hasattr(ast, 'unparse'):
- decorators.append(ast.unparse(decorator))
- elif isinstance(decorator, ast.Name):
- decorators.append(decorator.id)
- except:
- pass
-
- # Extract docstring
- docstring = ast.get_docstring(node)
-
- return FunctionSignature(
- name=node.name,
- parameters=params,
- return_type=return_type,
- docstring=docstring,
- line_number=node.lineno,
- is_async=isinstance(node, ast.AsyncFunctionDef),
- is_method=is_method,
- decorators=decorators
- )
-
- def _analyze_javascript(self, content: str, file_path: str) -> Dict[str, Any]:
- """
- Analyze JavaScript/TypeScript file using regex patterns.
-
- Note: This is a simplified approach. For production, consider using
- a proper JS/TS parser like esprima or ts-morph.
- """
- classes = []
- functions = []
-
- # Extract class definitions
- class_pattern = r'class\s+(\w+)(?:\s+extends\s+(\w+))?\s*\{'
- for match in re.finditer(class_pattern, content):
- class_name = match.group(1)
- base_class = match.group(2) if match.group(2) else None
-
- # Try to extract methods (simplified)
- class_block_start = match.end()
- # This is a simplification - proper parsing would track braces
- class_block_end = content.find('}', class_block_start)
- if class_block_end != -1:
- class_body = content[class_block_start:class_block_end]
- methods = self._extract_js_methods(class_body)
- else:
- methods = []
-
- classes.append({
- 'name': class_name,
- 'base_classes': [base_class] if base_class else [],
- 'methods': methods,
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1
- })
-
- # Extract top-level functions
- func_pattern = r'(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)'
- for match in re.finditer(func_pattern, content):
- func_name = match.group(1)
- params_str = match.group(2)
- is_async = 'async' in match.group(0)
-
- params = self._parse_js_parameters(params_str)
-
- functions.append({
- 'name': func_name,
- 'parameters': params,
- 'return_type': None, # JS doesn't have type annotations (unless TS)
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1,
- 'is_async': is_async,
- 'is_method': False,
- 'decorators': []
- })
-
- # Extract arrow functions assigned to const/let
- arrow_pattern = r'(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>'
- for match in re.finditer(arrow_pattern, content):
- func_name = match.group(1)
- params_str = match.group(2)
- is_async = 'async' in match.group(0)
-
- params = self._parse_js_parameters(params_str)
-
- functions.append({
- 'name': func_name,
- 'parameters': params,
- 'return_type': None,
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1,
- 'is_async': is_async,
- 'is_method': False,
- 'decorators': []
- })
-
- return {
- 'classes': classes,
- 'functions': functions
- }
-
- def _extract_js_methods(self, class_body: str) -> List[Dict]:
- """Extract method signatures from class body."""
- methods = []
-
- # Match method definitions
- method_pattern = r'(?:async\s+)?(\w+)\s*\(([^)]*)\)'
- for match in re.finditer(method_pattern, class_body):
- method_name = match.group(1)
- params_str = match.group(2)
- is_async = 'async' in match.group(0)
-
- # Skip constructor keyword detection
- if method_name in ['if', 'for', 'while', 'switch']:
- continue
-
- params = self._parse_js_parameters(params_str)
-
- methods.append({
- 'name': method_name,
- 'parameters': params,
- 'return_type': None,
- 'docstring': None,
- 'line_number': None,
- 'is_async': is_async,
- 'is_method': True,
- 'decorators': []
- })
-
- return methods
-
- def _parse_js_parameters(self, params_str: str) -> List[Dict]:
- """Parse JavaScript parameter string."""
- params = []
-
- if not params_str.strip():
- return params
-
- # Split by comma (simplified - doesn't handle complex default values)
- param_list = [p.strip() for p in params_str.split(',')]
-
- for param in param_list:
- if not param:
- continue
-
- # Check for default value
- if '=' in param:
- name, default = param.split('=', 1)
- name = name.strip()
- default = default.strip()
- else:
- name = param
- default = None
-
- # Check for type annotation (TypeScript)
- type_hint = None
- if ':' in name:
- name, type_hint = name.split(':', 1)
- name = name.strip()
- type_hint = type_hint.strip()
-
- params.append({
- 'name': name,
- 'type_hint': type_hint,
- 'default': default
- })
-
- return params
-
- def _analyze_cpp(self, content: str, file_path: str) -> Dict[str, Any]:
- """
- Analyze C/C++ header file using regex patterns.
-
- Note: This is a simplified approach focusing on header files.
- For production, consider using libclang or similar.
- """
- classes = []
- functions = []
-
- # Extract class definitions (simplified - doesn't handle nested classes)
- class_pattern = r'class\s+(\w+)(?:\s*:\s*public\s+(\w+))?\s*\{'
- for match in re.finditer(class_pattern, content):
- class_name = match.group(1)
- base_class = match.group(2) if match.group(2) else None
-
- classes.append({
- 'name': class_name,
- 'base_classes': [base_class] if base_class else [],
- 'methods': [], # Simplified - would need to parse class body
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1
- })
-
- # Extract function declarations
- func_pattern = r'(\w+(?:\s*\*|\s*&)?)\s+(\w+)\s*\(([^)]*)\)'
- for match in re.finditer(func_pattern, content):
- return_type = match.group(1).strip()
- func_name = match.group(2)
- params_str = match.group(3)
-
- # Skip common keywords
- if func_name in ['if', 'for', 'while', 'switch', 'return']:
- continue
-
- params = self._parse_cpp_parameters(params_str)
-
- functions.append({
- 'name': func_name,
- 'parameters': params,
- 'return_type': return_type,
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1,
- 'is_async': False,
- 'is_method': False,
- 'decorators': []
- })
-
- return {
- 'classes': classes,
- 'functions': functions
- }
-
- def _parse_cpp_parameters(self, params_str: str) -> List[Dict]:
- """Parse C++ parameter string."""
- params = []
-
- if not params_str.strip() or params_str.strip() == 'void':
- return params
-
- # Split by comma (simplified)
- param_list = [p.strip() for p in params_str.split(',')]
-
- for param in param_list:
- if not param:
- continue
-
- # Check for default value
- default = None
- if '=' in param:
- param, default = param.rsplit('=', 1)
- param = param.strip()
- default = default.strip()
-
- # Extract type and name (simplified)
- # Format: "type name" or "type* name" or "type& name"
- parts = param.split()
- if len(parts) >= 2:
- param_type = ' '.join(parts[:-1])
- param_name = parts[-1]
- else:
- param_type = param
- param_name = "unknown"
-
- params.append({
- 'name': param_name,
- 'type_hint': param_type,
- 'default': default
- })
-
- return params
-
-
-if __name__ == '__main__':
- # Test the analyzer
- python_code = '''
-class Node2D:
- """Base class for 2D nodes."""
-
- def move_local_x(self, delta: float, snap: bool = False) -> None:
- """Move node along local X axis."""
- pass
-
- async def tween_position(self, target: tuple, duration: float = 1.0):
- """Animate position to target."""
- pass
-
-def create_sprite(texture: str) -> Node2D:
- """Create a new sprite node."""
- return Node2D()
-'''
-
- analyzer = CodeAnalyzer(depth='deep')
- result = analyzer.analyze_file('test.py', python_code, 'Python')
-
- print("Analysis Result:")
- print(f"Classes: {len(result.get('classes', []))}")
- print(f"Functions: {len(result.get('functions', []))}")
-
- if result.get('classes'):
- cls = result['classes'][0]
- print(f"\nClass: {cls['name']}")
- print(f" Methods: {len(cls['methods'])}")
- for method in cls['methods']:
- params = ', '.join([f"{p['name']}: {p['type_hint']}" + (f" = {p['default']}" if p.get('default') else "")
- for p in method['parameters']])
- print(f" {method['name']}({params}) -> {method['return_type']}")
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/config_validator.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/config_validator.py
deleted file mode 100644
index b8391de..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/config_validator.py
+++ /dev/null
@@ -1,376 +0,0 @@
-#!/usr/bin/env python3
-"""
-Unified Config Validator
-
-Validates unified config format that supports multiple sources:
-- documentation (website scraping)
-- github (repository scraping)
-- pdf (PDF document scraping)
-
-Also provides backward compatibility detection for legacy configs.
-"""
-
-import json
-import logging
-from typing import Dict, Any, List, Optional, Union
-from pathlib import Path
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-class ConfigValidator:
- """
- Validates unified config format and provides backward compatibility.
- """
-
- # Valid source types
- VALID_SOURCE_TYPES = {'documentation', 'github', 'pdf'}
-
- # Valid merge modes
- VALID_MERGE_MODES = {'rule-based', 'claude-enhanced'}
-
- # Valid code analysis depth levels
- VALID_DEPTH_LEVELS = {'surface', 'deep', 'full'}
-
- def __init__(self, config_or_path: Union[Dict[str, Any], str]):
- """
- Initialize validator with config dict or file path.
-
- Args:
- config_or_path: Either a config dict or path to config JSON file
- """
- if isinstance(config_or_path, dict):
- self.config_path = None
- self.config = config_or_path
- else:
- self.config_path = config_or_path
- self.config = self._load_config()
- self.is_unified = self._detect_format()
-
- def _load_config(self) -> Dict[str, Any]:
- """Load JSON config file."""
- try:
- with open(self.config_path, 'r', encoding='utf-8') as f:
- return json.load(f)
- except FileNotFoundError:
- raise ValueError(f"Config file not found: {self.config_path}")
- except json.JSONDecodeError as e:
- raise ValueError(f"Invalid JSON in config file: {e}")
-
- def _detect_format(self) -> bool:
- """
- Detect if config is unified format or legacy.
-
- Returns:
- True if unified format (has 'sources' array)
- False if legacy format
- """
- return 'sources' in self.config and isinstance(self.config['sources'], list)
-
- def validate(self) -> bool:
- """
- Validate config based on detected format.
-
- Returns:
- True if valid
-
- Raises:
- ValueError if invalid with detailed error message
- """
- if self.is_unified:
- return self._validate_unified()
- else:
- return self._validate_legacy()
-
- def _validate_unified(self) -> bool:
- """Validate unified config format."""
- logger.info("Validating unified config format...")
-
- # Required top-level fields
- if 'name' not in self.config:
- raise ValueError("Missing required field: 'name'")
-
- if 'description' not in self.config:
- raise ValueError("Missing required field: 'description'")
-
- if 'sources' not in self.config:
- raise ValueError("Missing required field: 'sources'")
-
- # Validate sources array
- sources = self.config['sources']
-
- if not isinstance(sources, list):
- raise ValueError("'sources' must be an array")
-
- if len(sources) == 0:
- raise ValueError("'sources' array cannot be empty")
-
- # Validate merge_mode (optional)
- merge_mode = self.config.get('merge_mode', 'rule-based')
- if merge_mode not in self.VALID_MERGE_MODES:
- raise ValueError(f"Invalid merge_mode: '{merge_mode}'. Must be one of {self.VALID_MERGE_MODES}")
-
- # Validate each source
- for i, source in enumerate(sources):
- self._validate_source(source, i)
-
- logger.info(f"✅ Unified config valid: {len(sources)} sources")
- return True
-
- def _validate_source(self, source: Dict[str, Any], index: int):
- """Validate individual source configuration."""
- # Check source has 'type' field
- if 'type' not in source:
- raise ValueError(f"Source {index}: Missing required field 'type'")
-
- source_type = source['type']
-
- if source_type not in self.VALID_SOURCE_TYPES:
- raise ValueError(
- f"Source {index}: Invalid type '{source_type}'. "
- f"Must be one of {self.VALID_SOURCE_TYPES}"
- )
-
- # Type-specific validation
- if source_type == 'documentation':
- self._validate_documentation_source(source, index)
- elif source_type == 'github':
- self._validate_github_source(source, index)
- elif source_type == 'pdf':
- self._validate_pdf_source(source, index)
-
- def _validate_documentation_source(self, source: Dict[str, Any], index: int):
- """Validate documentation source configuration."""
- if 'base_url' not in source:
- raise ValueError(f"Source {index} (documentation): Missing required field 'base_url'")
-
- # Optional but recommended fields
- if 'selectors' not in source:
- logger.warning(f"Source {index} (documentation): No 'selectors' specified, using defaults")
-
- if 'max_pages' in source and not isinstance(source['max_pages'], int):
- raise ValueError(f"Source {index} (documentation): 'max_pages' must be an integer")
-
- def _validate_github_source(self, source: Dict[str, Any], index: int):
- """Validate GitHub source configuration."""
- if 'repo' not in source:
- raise ValueError(f"Source {index} (github): Missing required field 'repo'")
-
- # Validate repo format (owner/repo)
- repo = source['repo']
- if '/' not in repo:
- raise ValueError(
- f"Source {index} (github): Invalid repo format '{repo}'. "
- f"Must be 'owner/repo' (e.g., 'facebook/react')"
- )
-
- # Validate code_analysis_depth if specified
- if 'code_analysis_depth' in source:
- depth = source['code_analysis_depth']
- if depth not in self.VALID_DEPTH_LEVELS:
- raise ValueError(
- f"Source {index} (github): Invalid code_analysis_depth '{depth}'. "
- f"Must be one of {self.VALID_DEPTH_LEVELS}"
- )
-
- # Validate max_issues if specified
- if 'max_issues' in source and not isinstance(source['max_issues'], int):
- raise ValueError(f"Source {index} (github): 'max_issues' must be an integer")
-
- def _validate_pdf_source(self, source: Dict[str, Any], index: int):
- """Validate PDF source configuration."""
- if 'path' not in source:
- raise ValueError(f"Source {index} (pdf): Missing required field 'path'")
-
- # Check if file exists
- pdf_path = source['path']
- if not Path(pdf_path).exists():
- logger.warning(f"Source {index} (pdf): File not found: {pdf_path}")
-
- def _validate_legacy(self) -> bool:
- """
- Validate legacy config format (backward compatibility).
-
- Legacy configs are the old format used by doc_scraper, github_scraper, pdf_scraper.
- """
- logger.info("Detected legacy config format (backward compatible)")
-
- # Detect which legacy type based on fields
- if 'base_url' in self.config:
- logger.info("Legacy type: documentation")
- elif 'repo' in self.config:
- logger.info("Legacy type: github")
- elif 'pdf' in self.config or 'path' in self.config:
- logger.info("Legacy type: pdf")
- else:
- raise ValueError("Cannot detect legacy config type (missing base_url, repo, or pdf)")
-
- return True
-
- def convert_legacy_to_unified(self) -> Dict[str, Any]:
- """
- Convert legacy config to unified format.
-
- Returns:
- Unified config dict
- """
- if self.is_unified:
- logger.info("Config already in unified format")
- return self.config
-
- logger.info("Converting legacy config to unified format...")
-
- # Detect legacy type and convert
- if 'base_url' in self.config:
- return self._convert_legacy_documentation()
- elif 'repo' in self.config:
- return self._convert_legacy_github()
- elif 'pdf' in self.config or 'path' in self.config:
- return self._convert_legacy_pdf()
- else:
- raise ValueError("Cannot convert: unknown legacy format")
-
- def _convert_legacy_documentation(self) -> Dict[str, Any]:
- """Convert legacy documentation config to unified."""
- unified = {
- 'name': self.config.get('name', 'unnamed'),
- 'description': self.config.get('description', 'Documentation skill'),
- 'merge_mode': 'rule-based',
- 'sources': [
- {
- 'type': 'documentation',
- **{k: v for k, v in self.config.items()
- if k not in ['name', 'description']}
- }
- ]
- }
- return unified
-
- def _convert_legacy_github(self) -> Dict[str, Any]:
- """Convert legacy GitHub config to unified."""
- unified = {
- 'name': self.config.get('name', 'unnamed'),
- 'description': self.config.get('description', 'GitHub repository skill'),
- 'merge_mode': 'rule-based',
- 'sources': [
- {
- 'type': 'github',
- **{k: v for k, v in self.config.items()
- if k not in ['name', 'description']}
- }
- ]
- }
- return unified
-
- def _convert_legacy_pdf(self) -> Dict[str, Any]:
- """Convert legacy PDF config to unified."""
- unified = {
- 'name': self.config.get('name', 'unnamed'),
- 'description': self.config.get('description', 'PDF document skill'),
- 'merge_mode': 'rule-based',
- 'sources': [
- {
- 'type': 'pdf',
- **{k: v for k, v in self.config.items()
- if k not in ['name', 'description']}
- }
- ]
- }
- return unified
-
- def get_sources_by_type(self, source_type: str) -> List[Dict[str, Any]]:
- """
- Get all sources of a specific type.
-
- Args:
- source_type: 'documentation', 'github', or 'pdf'
-
- Returns:
- List of sources matching the type
- """
- if not self.is_unified:
- # For legacy, convert and get sources
- unified = self.convert_legacy_to_unified()
- sources = unified['sources']
- else:
- sources = self.config['sources']
-
- return [s for s in sources if s.get('type') == source_type]
-
- def has_multiple_sources(self) -> bool:
- """Check if config has multiple sources (requires merging)."""
- if not self.is_unified:
- return False
- return len(self.config['sources']) > 1
-
- def needs_api_merge(self) -> bool:
- """
- Check if config needs API merging.
-
- Returns True if both documentation and github sources exist
- with API extraction enabled.
- """
- if not self.has_multiple_sources():
- return False
-
- has_docs_api = any(
- s.get('type') == 'documentation' and s.get('extract_api', True)
- for s in self.config['sources']
- )
-
- has_github_code = any(
- s.get('type') == 'github' and s.get('include_code', False)
- for s in self.config['sources']
- )
-
- return has_docs_api and has_github_code
-
-
-def validate_config(config_path: str) -> ConfigValidator:
- """
- Validate config file and return validator instance.
-
- Args:
- config_path: Path to config JSON file
-
- Returns:
- ConfigValidator instance
-
- Raises:
- ValueError if config is invalid
- """
- validator = ConfigValidator(config_path)
- validator.validate()
- return validator
-
-
-if __name__ == '__main__':
- import sys
-
- if len(sys.argv) < 2:
- print("Usage: python config_validator.py ")
- sys.exit(1)
-
- config_file = sys.argv[1]
-
- try:
- validator = validate_config(config_file)
-
- print(f"\n✅ Config valid!")
- print(f" Format: {'Unified' if validator.is_unified else 'Legacy'}")
- print(f" Name: {validator.config.get('name')}")
-
- if validator.is_unified:
- sources = validator.config['sources']
- print(f" Sources: {len(sources)}")
- for i, source in enumerate(sources):
- print(f" {i+1}. {source['type']}")
-
- if validator.needs_api_merge():
- merge_mode = validator.config.get('merge_mode', 'rule-based')
- print(f" ⚠️ API merge required (mode: {merge_mode})")
-
- except ValueError as e:
- print(f"\n❌ Config invalid: {e}")
- sys.exit(1)
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/conflict_detector.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/conflict_detector.py
deleted file mode 100644
index 5f7d4c2..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/conflict_detector.py
+++ /dev/null
@@ -1,513 +0,0 @@
-#!/usr/bin/env python3
-"""
-Conflict Detector for Multi-Source Skills
-
-Detects conflicts between documentation and code:
-- missing_in_docs: API exists in code but not documented
-- missing_in_code: API documented but doesn't exist in code
-- signature_mismatch: Different parameters/types between docs and code
-- description_mismatch: Docs say one thing, code comments say another
-
-Used by unified scraper to identify discrepancies before merging.
-"""
-
-import json
-import logging
-from typing import Dict, List, Any, Optional, Tuple
-from dataclasses import dataclass, asdict
-from difflib import SequenceMatcher
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class Conflict:
- """Represents a conflict between documentation and code."""
- type: str # 'missing_in_docs', 'missing_in_code', 'signature_mismatch', 'description_mismatch'
- severity: str # 'low', 'medium', 'high'
- api_name: str
- docs_info: Optional[Dict[str, Any]] = None
- code_info: Optional[Dict[str, Any]] = None
- difference: Optional[str] = None
- suggestion: Optional[str] = None
-
-
-class ConflictDetector:
- """
- Detects conflicts between documentation and code sources.
- """
-
- def __init__(self, docs_data: Dict[str, Any], github_data: Dict[str, Any]):
- """
- Initialize conflict detector.
-
- Args:
- docs_data: Data from documentation scraper
- github_data: Data from GitHub scraper with code analysis
- """
- self.docs_data = docs_data
- self.github_data = github_data
-
- # Extract API information from both sources
- self.docs_apis = self._extract_docs_apis()
- self.code_apis = self._extract_code_apis()
-
- logger.info(f"Loaded {len(self.docs_apis)} APIs from documentation")
- logger.info(f"Loaded {len(self.code_apis)} APIs from code")
-
- def _extract_docs_apis(self) -> Dict[str, Dict[str, Any]]:
- """
- Extract API information from documentation data.
-
- Returns:
- Dict mapping API name to API info
- """
- apis = {}
-
- # Documentation structure varies, but typically has 'pages' or 'references'
- pages = self.docs_data.get('pages', {})
-
- # Handle both dict and list formats
- if isinstance(pages, dict):
- # Format: {url: page_data, ...}
- for url, page_data in pages.items():
- content = page_data.get('content', '')
- title = page_data.get('title', '')
-
- # Simple heuristic: if title or URL contains "api", "reference", "class", "function"
- # it might be an API page
- if any(keyword in title.lower() or keyword in url.lower()
- for keyword in ['api', 'reference', 'class', 'function', 'method']):
-
- # Extract API signatures from content (simplified)
- extracted_apis = self._parse_doc_content_for_apis(content, url)
- apis.update(extracted_apis)
- elif isinstance(pages, list):
- # Format: [{url: '...', apis: [...]}, ...]
- for page in pages:
- url = page.get('url', '')
- page_apis = page.get('apis', [])
-
- # If APIs are already extracted in the page data
- for api in page_apis:
- api_name = api.get('name', '')
- if api_name:
- apis[api_name] = {
- 'parameters': api.get('parameters', []),
- 'return_type': api.get('return_type', 'Any'),
- 'source_url': url
- }
-
- return apis
-
- def _parse_doc_content_for_apis(self, content: str, source_url: str) -> Dict[str, Dict]:
- """
- Parse documentation content to extract API signatures.
-
- This is a simplified approach - real implementation would need
- to understand the documentation format (Sphinx, JSDoc, etc.)
- """
- apis = {}
-
- # Look for function/method signatures in code blocks
- # Common patterns:
- # - function_name(param1, param2)
- # - ClassName.method_name(param1, param2)
- # - def function_name(param1: type, param2: type) -> return_type
-
- import re
-
- # Pattern for common API signatures
- patterns = [
- # Python style: def name(params) -> return
- r'def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?',
- # JavaScript style: function name(params)
- r'function\s+(\w+)\s*\(([^)]*)\)',
- # C++ style: return_type name(params)
- r'(\w+)\s+(\w+)\s*\(([^)]*)\)',
- # Method style: ClassName.method_name(params)
- r'(\w+)\.(\w+)\s*\(([^)]*)\)'
- ]
-
- for pattern in patterns:
- for match in re.finditer(pattern, content):
- groups = match.groups()
-
- # Parse based on pattern matched
- if 'def' in pattern:
- # Python function
- name = groups[0]
- params_str = groups[1]
- return_type = groups[2] if len(groups) > 2 else None
- elif 'function' in pattern:
- # JavaScript function
- name = groups[0]
- params_str = groups[1]
- return_type = None
- elif '.' in pattern:
- # Class method
- class_name = groups[0]
- method_name = groups[1]
- name = f"{class_name}.{method_name}"
- params_str = groups[2] if len(groups) > 2 else groups[1]
- return_type = None
- else:
- # C++ function
- return_type = groups[0]
- name = groups[1]
- params_str = groups[2]
-
- # Parse parameters
- params = self._parse_param_string(params_str)
-
- apis[name] = {
- 'name': name,
- 'parameters': params,
- 'return_type': return_type,
- 'source': source_url,
- 'raw_signature': match.group(0)
- }
-
- return apis
-
- def _parse_param_string(self, params_str: str) -> List[Dict]:
- """Parse parameter string into list of parameter dicts."""
- if not params_str.strip():
- return []
-
- params = []
- for param in params_str.split(','):
- param = param.strip()
- if not param:
- continue
-
- # Try to extract name and type
- param_info = {'name': param, 'type': None, 'default': None}
-
- # Check for type annotation (: type)
- if ':' in param:
- parts = param.split(':', 1)
- param_info['name'] = parts[0].strip()
- type_part = parts[1].strip()
-
- # Check for default value (= value)
- if '=' in type_part:
- type_str, default_str = type_part.split('=', 1)
- param_info['type'] = type_str.strip()
- param_info['default'] = default_str.strip()
- else:
- param_info['type'] = type_part
-
- # Check for default without type (= value)
- elif '=' in param:
- parts = param.split('=', 1)
- param_info['name'] = parts[0].strip()
- param_info['default'] = parts[1].strip()
-
- params.append(param_info)
-
- return params
-
- def _extract_code_apis(self) -> Dict[str, Dict[str, Any]]:
- """
- Extract API information from GitHub code analysis.
-
- Returns:
- Dict mapping API name to API info
- """
- apis = {}
-
- code_analysis = self.github_data.get('code_analysis', {})
- if not code_analysis:
- return apis
-
- # Support both 'files' and 'analyzed_files' keys
- files = code_analysis.get('files', code_analysis.get('analyzed_files', []))
-
- for file_info in files:
- file_path = file_info.get('file', 'unknown')
-
- # Extract classes and their methods
- for class_info in file_info.get('classes', []):
- class_name = class_info['name']
-
- # Add class itself
- apis[class_name] = {
- 'name': class_name,
- 'type': 'class',
- 'source': file_path,
- 'line': class_info.get('line_number'),
- 'base_classes': class_info.get('base_classes', []),
- 'docstring': class_info.get('docstring')
- }
-
- # Add methods
- for method in class_info.get('methods', []):
- method_name = f"{class_name}.{method['name']}"
- apis[method_name] = {
- 'name': method_name,
- 'type': 'method',
- 'parameters': method.get('parameters', []),
- 'return_type': method.get('return_type'),
- 'source': file_path,
- 'line': method.get('line_number'),
- 'docstring': method.get('docstring'),
- 'is_async': method.get('is_async', False)
- }
-
- # Extract standalone functions
- for func_info in file_info.get('functions', []):
- func_name = func_info['name']
- apis[func_name] = {
- 'name': func_name,
- 'type': 'function',
- 'parameters': func_info.get('parameters', []),
- 'return_type': func_info.get('return_type'),
- 'source': file_path,
- 'line': func_info.get('line_number'),
- 'docstring': func_info.get('docstring'),
- 'is_async': func_info.get('is_async', False)
- }
-
- return apis
-
- def detect_all_conflicts(self) -> List[Conflict]:
- """
- Detect all types of conflicts.
-
- Returns:
- List of Conflict objects
- """
- logger.info("Detecting conflicts between documentation and code...")
-
- conflicts = []
-
- # 1. Find APIs missing in documentation
- conflicts.extend(self._find_missing_in_docs())
-
- # 2. Find APIs missing in code
- conflicts.extend(self._find_missing_in_code())
-
- # 3. Find signature mismatches
- conflicts.extend(self._find_signature_mismatches())
-
- logger.info(f"Found {len(conflicts)} conflicts total")
-
- return conflicts
-
- def _find_missing_in_docs(self) -> List[Conflict]:
- """Find APIs that exist in code but not in documentation."""
- conflicts = []
-
- for api_name, code_info in self.code_apis.items():
- # Simple name matching (can be enhanced with fuzzy matching)
- if api_name not in self.docs_apis:
- # Check if it's a private/internal API (often not documented)
- is_private = api_name.startswith('_') or '__' in api_name
- severity = 'low' if is_private else 'medium'
-
- conflicts.append(Conflict(
- type='missing_in_docs',
- severity=severity,
- api_name=api_name,
- code_info=code_info,
- difference=f"API exists in code ({code_info['source']}) but not found in documentation",
- suggestion="Add documentation for this API" if not is_private else "Consider if this internal API should be documented"
- ))
-
- logger.info(f"Found {len(conflicts)} APIs missing in documentation")
- return conflicts
-
- def _find_missing_in_code(self) -> List[Conflict]:
- """Find APIs that are documented but don't exist in code."""
- conflicts = []
-
- for api_name, docs_info in self.docs_apis.items():
- if api_name not in self.code_apis:
- conflicts.append(Conflict(
- type='missing_in_code',
- severity='high', # This is serious - documented but doesn't exist
- api_name=api_name,
- docs_info=docs_info,
- difference=f"API documented ({docs_info.get('source', 'unknown')}) but not found in code",
- suggestion="Update documentation to remove this API, or add it to codebase"
- ))
-
- logger.info(f"Found {len(conflicts)} APIs missing in code")
- return conflicts
-
- def _find_signature_mismatches(self) -> List[Conflict]:
- """Find APIs where signature differs between docs and code."""
- conflicts = []
-
- # Find APIs that exist in both
- common_apis = set(self.docs_apis.keys()) & set(self.code_apis.keys())
-
- for api_name in common_apis:
- docs_info = self.docs_apis[api_name]
- code_info = self.code_apis[api_name]
-
- # Compare signatures
- mismatch = self._compare_signatures(docs_info, code_info)
-
- if mismatch:
- conflicts.append(Conflict(
- type='signature_mismatch',
- severity=mismatch['severity'],
- api_name=api_name,
- docs_info=docs_info,
- code_info=code_info,
- difference=mismatch['difference'],
- suggestion=mismatch['suggestion']
- ))
-
- logger.info(f"Found {len(conflicts)} signature mismatches")
- return conflicts
-
- def _compare_signatures(self, docs_info: Dict, code_info: Dict) -> Optional[Dict]:
- """
- Compare signatures between docs and code.
-
- Returns:
- Dict with mismatch details if conflict found, None otherwise
- """
- docs_params = docs_info.get('parameters', [])
- code_params = code_info.get('parameters', [])
-
- # Compare parameter counts
- if len(docs_params) != len(code_params):
- return {
- 'severity': 'medium',
- 'difference': f"Parameter count mismatch: docs has {len(docs_params)}, code has {len(code_params)}",
- 'suggestion': f"Documentation shows {len(docs_params)} parameters, but code has {len(code_params)}"
- }
-
- # Compare parameter names and types
- for i, (doc_param, code_param) in enumerate(zip(docs_params, code_params)):
- doc_name = doc_param.get('name', '')
- code_name = code_param.get('name', '')
-
- # Parameter name mismatch
- if doc_name != code_name:
- # Use fuzzy matching for slight variations
- similarity = SequenceMatcher(None, doc_name, code_name).ratio()
- if similarity < 0.8: # Not similar enough
- return {
- 'severity': 'medium',
- 'difference': f"Parameter {i+1} name mismatch: '{doc_name}' in docs vs '{code_name}' in code",
- 'suggestion': f"Update documentation to use parameter name '{code_name}'"
- }
-
- # Type mismatch
- doc_type = doc_param.get('type')
- code_type = code_param.get('type_hint')
-
- if doc_type and code_type and doc_type != code_type:
- return {
- 'severity': 'low',
- 'difference': f"Parameter '{doc_name}' type mismatch: '{doc_type}' in docs vs '{code_type}' in code",
- 'suggestion': f"Verify correct type for parameter '{doc_name}'"
- }
-
- # Compare return types if both have them
- docs_return = docs_info.get('return_type')
- code_return = code_info.get('return_type')
-
- if docs_return and code_return and docs_return != code_return:
- return {
- 'severity': 'low',
- 'difference': f"Return type mismatch: '{docs_return}' in docs vs '{code_return}' in code",
- 'suggestion': "Verify correct return type"
- }
-
- return None
-
- def generate_summary(self, conflicts: List[Conflict]) -> Dict[str, Any]:
- """
- Generate summary statistics for conflicts.
-
- Args:
- conflicts: List of Conflict objects
-
- Returns:
- Summary dict with statistics
- """
- summary = {
- 'total': len(conflicts),
- 'by_type': {},
- 'by_severity': {},
- 'apis_affected': len(set(c.api_name for c in conflicts))
- }
-
- # Count by type
- for conflict_type in ['missing_in_docs', 'missing_in_code', 'signature_mismatch', 'description_mismatch']:
- count = sum(1 for c in conflicts if c.type == conflict_type)
- summary['by_type'][conflict_type] = count
-
- # Count by severity
- for severity in ['low', 'medium', 'high']:
- count = sum(1 for c in conflicts if c.severity == severity)
- summary['by_severity'][severity] = count
-
- return summary
-
- def save_conflicts(self, conflicts: List[Conflict], output_path: str):
- """
- Save conflicts to JSON file.
-
- Args:
- conflicts: List of Conflict objects
- output_path: Path to output JSON file
- """
- data = {
- 'conflicts': [asdict(c) for c in conflicts],
- 'summary': self.generate_summary(conflicts)
- }
-
- with open(output_path, 'w', encoding='utf-8') as f:
- json.dump(data, f, indent=2, ensure_ascii=False)
-
- logger.info(f"Conflicts saved to: {output_path}")
-
-
-if __name__ == '__main__':
- import sys
-
- if len(sys.argv) < 3:
- print("Usage: python conflict_detector.py ")
- sys.exit(1)
-
- docs_file = sys.argv[1]
- github_file = sys.argv[2]
-
- # Load data
- with open(docs_file, 'r') as f:
- docs_data = json.load(f)
-
- with open(github_file, 'r') as f:
- github_data = json.load(f)
-
- # Detect conflicts
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector.detect_all_conflicts()
-
- # Print summary
- summary = detector.generate_summary(conflicts)
- print("\n📊 Conflict Summary:")
- print(f" Total conflicts: {summary['total']}")
- print(f" APIs affected: {summary['apis_affected']}")
- print("\n By Type:")
- for conflict_type, count in summary['by_type'].items():
- if count > 0:
- print(f" {conflict_type}: {count}")
- print("\n By Severity:")
- for severity, count in summary['by_severity'].items():
- if count > 0:
- emoji = '🔴' if severity == 'high' else '🟡' if severity == 'medium' else '🟢'
- print(f" {emoji} {severity}: {count}")
-
- # Save to file
- output_file = 'conflicts.json'
- detector.save_conflicts(conflicts, output_file)
- print(f"\n✅ Full report saved to: {output_file}")
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/constants.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/constants.py
deleted file mode 100644
index 2685e93..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/constants.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""Configuration constants for Skill Seekers CLI.
-
-This module centralizes all magic numbers and configuration values used
-across the CLI tools to improve maintainability and clarity.
-"""
-
-# ===== SCRAPING CONFIGURATION =====
-
-# Default scraping limits
-DEFAULT_RATE_LIMIT = 0.5 # seconds between requests
-DEFAULT_MAX_PAGES = 500 # maximum pages to scrape
-DEFAULT_CHECKPOINT_INTERVAL = 1000 # pages between checkpoints
-DEFAULT_ASYNC_MODE = False # use async mode for parallel scraping (opt-in)
-
-# Content analysis limits
-CONTENT_PREVIEW_LENGTH = 500 # characters to check for categorization
-MAX_PAGES_WARNING_THRESHOLD = 10000 # warn if config exceeds this
-
-# Quality thresholds
-MIN_CATEGORIZATION_SCORE = 2 # minimum score for category assignment
-URL_MATCH_POINTS = 3 # points for URL keyword match
-TITLE_MATCH_POINTS = 2 # points for title keyword match
-CONTENT_MATCH_POINTS = 1 # points for content keyword match
-
-# ===== ENHANCEMENT CONFIGURATION =====
-
-# API-based enhancement limits (uses Anthropic API)
-API_CONTENT_LIMIT = 100000 # max characters for API enhancement
-API_PREVIEW_LIMIT = 40000 # max characters for preview
-
-# Local enhancement limits (uses Claude Code Max)
-LOCAL_CONTENT_LIMIT = 50000 # max characters for local enhancement
-LOCAL_PREVIEW_LIMIT = 20000 # max characters for preview
-
-# ===== PAGE ESTIMATION =====
-
-# Estimation and discovery settings
-DEFAULT_MAX_DISCOVERY = 1000 # default max pages to discover
-DISCOVERY_THRESHOLD = 10000 # threshold for warnings
-
-# ===== FILE LIMITS =====
-
-# Output and processing limits
-MAX_REFERENCE_FILES = 100 # maximum reference files per skill
-MAX_CODE_BLOCKS_PER_PAGE = 5 # maximum code blocks to extract per page
-
-# ===== EXPORT CONSTANTS =====
-
-__all__ = [
- # Scraping
- 'DEFAULT_RATE_LIMIT',
- 'DEFAULT_MAX_PAGES',
- 'DEFAULT_CHECKPOINT_INTERVAL',
- 'DEFAULT_ASYNC_MODE',
- 'CONTENT_PREVIEW_LENGTH',
- 'MAX_PAGES_WARNING_THRESHOLD',
- 'MIN_CATEGORIZATION_SCORE',
- 'URL_MATCH_POINTS',
- 'TITLE_MATCH_POINTS',
- 'CONTENT_MATCH_POINTS',
- # Enhancement
- 'API_CONTENT_LIMIT',
- 'API_PREVIEW_LIMIT',
- 'LOCAL_CONTENT_LIMIT',
- 'LOCAL_PREVIEW_LIMIT',
- # Estimation
- 'DEFAULT_MAX_DISCOVERY',
- 'DISCOVERY_THRESHOLD',
- # Limits
- 'MAX_REFERENCE_FILES',
- 'MAX_CODE_BLOCKS_PER_PAGE',
-]
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/doc_scraper.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/doc_scraper.py
deleted file mode 100644
index 963780d..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/doc_scraper.py
+++ /dev/null
@@ -1,1822 +0,0 @@
-#!/usr/bin/env python3
-"""
-Documentation to Claude Skill Converter
-Single tool to scrape any documentation and create high-quality Claude skills.
-
-Usage:
- skill-seekers scrape --interactive
- skill-seekers scrape --config configs/godot.json
- skill-seekers scrape --url https://react.dev/ --name react
-"""
-
-import os
-import sys
-import json
-import time
-import re
-import argparse
-import hashlib
-import logging
-import asyncio
-import requests
-import httpx
-from pathlib import Path
-from urllib.parse import urljoin, urlparse
-from bs4 import BeautifulSoup
-from collections import deque, defaultdict
-from typing import Optional, Dict, List, Tuple, Set, Deque, Any
-
-# Add parent directory to path for imports when run as script
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.llms_txt_detector import LlmsTxtDetector
-from skill_seekers.cli.llms_txt_parser import LlmsTxtParser
-from skill_seekers.cli.llms_txt_downloader import LlmsTxtDownloader
-from skill_seekers.cli.constants import (
- DEFAULT_RATE_LIMIT,
- DEFAULT_MAX_PAGES,
- DEFAULT_CHECKPOINT_INTERVAL,
- DEFAULT_ASYNC_MODE,
- CONTENT_PREVIEW_LENGTH,
- MAX_PAGES_WARNING_THRESHOLD,
- MIN_CATEGORIZATION_SCORE
-)
-
-# Configure logging
-logger = logging.getLogger(__name__)
-
-
-def setup_logging(verbose: bool = False, quiet: bool = False) -> None:
- """Configure logging based on verbosity level.
-
- Args:
- verbose: Enable DEBUG level logging
- quiet: Enable WARNING level logging only
- """
- if quiet:
- level = logging.WARNING
- elif verbose:
- level = logging.DEBUG
- else:
- level = logging.INFO
-
- logging.basicConfig(
- level=level,
- format='%(message)s',
- force=True
- )
-
-
-class DocToSkillConverter:
- def __init__(self, config: Dict[str, Any], dry_run: bool = False, resume: bool = False) -> None:
- self.config = config
- self.name = config['name']
- self.base_url = config['base_url']
- self.dry_run = dry_run
- self.resume = resume
-
- # Paths
- self.data_dir = f"output/{self.name}_data"
- self.skill_dir = f"output/{self.name}"
- self.checkpoint_file = f"{self.data_dir}/checkpoint.json"
-
- # Checkpoint config
- checkpoint_config = config.get('checkpoint', {})
- self.checkpoint_enabled = checkpoint_config.get('enabled', False)
- self.checkpoint_interval = checkpoint_config.get('interval', DEFAULT_CHECKPOINT_INTERVAL)
-
- # llms.txt detection state
- skip_llms_txt_value = config.get('skip_llms_txt', False)
- if not isinstance(skip_llms_txt_value, bool):
- logger.warning(
- "Invalid value for 'skip_llms_txt': %r (expected bool). Defaulting to False.",
- skip_llms_txt_value
- )
- self.skip_llms_txt = False
- else:
- self.skip_llms_txt = skip_llms_txt_value
- self.llms_txt_detected = False
- self.llms_txt_variant = None
- self.llms_txt_variants: List[str] = [] # Track all downloaded variants
-
- # Parallel scraping config
- self.workers = config.get('workers', 1)
- self.async_mode = config.get('async_mode', DEFAULT_ASYNC_MODE)
-
- # State
- self.visited_urls: set[str] = set()
- # Support multiple starting URLs
- start_urls = config.get('start_urls', [self.base_url])
- self.pending_urls = deque(start_urls)
- self.pages: List[Dict[str, Any]] = []
- self.pages_scraped = 0
-
- # Thread-safe lock for parallel scraping
- if self.workers > 1:
- import threading
- self.lock = threading.Lock()
-
- # Create directories (unless dry-run)
- if not dry_run:
- os.makedirs(f"{self.data_dir}/pages", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/references", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/scripts", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/assets", exist_ok=True)
-
- # Load checkpoint if resuming
- if resume and not dry_run:
- self.load_checkpoint()
-
- def is_valid_url(self, url: str) -> bool:
- """Check if URL should be scraped based on patterns.
-
- Args:
- url (str): URL to validate
-
- Returns:
- bool: True if URL matches include patterns and doesn't match exclude patterns
- """
- if not url.startswith(self.base_url):
- return False
-
- # Include patterns
- includes = self.config.get('url_patterns', {}).get('include', [])
- if includes and not any(pattern in url for pattern in includes):
- return False
-
- # Exclude patterns
- excludes = self.config.get('url_patterns', {}).get('exclude', [])
- if any(pattern in url for pattern in excludes):
- return False
-
- return True
-
- def save_checkpoint(self) -> None:
- """Save progress checkpoint"""
- if not self.checkpoint_enabled or self.dry_run:
- return
-
- checkpoint_data = {
- "config": self.config,
- "visited_urls": list(self.visited_urls),
- "pending_urls": list(self.pending_urls),
- "pages_scraped": self.pages_scraped,
- "last_updated": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
- "checkpoint_interval": self.checkpoint_interval
- }
-
- try:
- with open(self.checkpoint_file, 'w') as f:
- json.dump(checkpoint_data, f, indent=2)
- logger.info(" 💾 Checkpoint saved (%d pages)", self.pages_scraped)
- except Exception as e:
- logger.warning(" ⚠️ Failed to save checkpoint: %s", e)
-
- def load_checkpoint(self) -> None:
- """Load progress from checkpoint"""
- if not os.path.exists(self.checkpoint_file):
- logger.info("ℹ️ No checkpoint found, starting fresh")
- return
-
- try:
- with open(self.checkpoint_file, 'r') as f:
- checkpoint_data = json.load(f)
-
- self.visited_urls = set(checkpoint_data["visited_urls"])
- self.pending_urls = deque(checkpoint_data["pending_urls"])
- self.pages_scraped = checkpoint_data["pages_scraped"]
-
- logger.info("✅ Resumed from checkpoint")
- logger.info(" Pages already scraped: %d", self.pages_scraped)
- logger.info(" URLs visited: %d", len(self.visited_urls))
- logger.info(" URLs pending: %d", len(self.pending_urls))
- logger.info(" Last updated: %s", checkpoint_data['last_updated'])
- logger.info("")
-
- except Exception as e:
- logger.warning("⚠️ Failed to load checkpoint: %s", e)
- logger.info(" Starting fresh")
-
- def clear_checkpoint(self) -> None:
- """Remove checkpoint file"""
- if os.path.exists(self.checkpoint_file):
- try:
- os.remove(self.checkpoint_file)
- logger.info("✅ Checkpoint cleared")
- except Exception as e:
- logger.warning("⚠️ Failed to clear checkpoint: %s", e)
-
- def extract_content(self, soup: Any, url: str) -> Dict[str, Any]:
- """Extract content with improved code and pattern detection"""
- page = {
- 'url': url,
- 'title': '',
- 'content': '',
- 'headings': [],
- 'code_samples': [],
- 'patterns': [], # NEW: Extract common patterns
- 'links': []
- }
-
- selectors = self.config.get('selectors', {})
-
- # Extract title
- title_elem = soup.select_one(selectors.get('title', 'title'))
- if title_elem:
- page['title'] = self.clean_text(title_elem.get_text())
-
- # Find main content
- main_selector = selectors.get('main_content', 'div[role="main"]')
- main = soup.select_one(main_selector)
-
- if not main:
- logger.warning("⚠ No content: %s", url)
- return page
-
- # Extract headings with better structure
- for h in main.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']):
- text = self.clean_text(h.get_text())
- if text:
- page['headings'].append({
- 'level': h.name,
- 'text': text,
- 'id': h.get('id', '')
- })
-
- # Extract code with language detection
- code_selector = selectors.get('code_blocks', 'pre code')
- for code_elem in main.select(code_selector):
- code = code_elem.get_text()
- if len(code.strip()) > 10:
- # Try to detect language
- lang = self.detect_language(code_elem, code)
- page['code_samples'].append({
- 'code': code.strip(),
- 'language': lang
- })
-
- # Extract patterns (NEW: common code patterns)
- page['patterns'] = self.extract_patterns(main, page['code_samples'])
-
- # Extract paragraphs
- paragraphs = []
- for p in main.find_all('p'):
- text = self.clean_text(p.get_text())
- if text and len(text) > 20: # Skip very short paragraphs
- paragraphs.append(text)
-
- page['content'] = '\n\n'.join(paragraphs)
-
- # Extract links from entire page (not just main content)
- # This allows discovery of navigation links outside the main content area
- for link in soup.find_all('a', href=True):
- href = urljoin(url, link['href'])
- # Strip anchor fragments to avoid treating #anchors as separate pages
- href = href.split('#')[0]
- if self.is_valid_url(href) and href not in page['links']:
- page['links'].append(href)
-
- return page
-
- def _extract_language_from_classes(self, classes):
- """Extract language from class list
-
- Supports multiple patterns:
- - language-{lang} (e.g., "language-python")
- - lang-{lang} (e.g., "lang-javascript")
- - brush: {lang} (e.g., "brush: java")
- - bare language name (e.g., "python", "java")
-
- """
- # Define common programming languages
- known_languages = [
- "javascript", "java", "xml", "html", "python", "bash", "cpp", "typescript",
- "go", "rust", "php", "ruby", "swift", "kotlin", "csharp", "c", "sql",
- "yaml", "json", "markdown", "css", "scss", "sass", "jsx", "tsx", "vue",
- "shell", "powershell", "r", "scala", "dart", "perl", "lua", "elixir"
- ]
-
- for cls in classes:
- # Clean special characters (except word chars and hyphens)
- cls = re.sub(r'[^\w-]', '', cls)
-
- if 'language-' in cls:
- return cls.replace('language-', '')
-
- if 'lang-' in cls:
- return cls.replace('lang-', '')
-
- # Check for brush: pattern (e.g., "brush: java")
- if 'brush' in cls.lower():
- lang = cls.lower().replace('brush', '').strip()
- if lang in known_languages:
- return lang
-
- # Check for bare language name
- if cls in known_languages:
- return cls
-
- return None
-
- def detect_language(self, elem, code):
- """Detect programming language from code block"""
-
- # Check element classes
- lang = self._extract_language_from_classes(elem.get('class', []))
- if lang:
- return lang
-
- # Check parent pre element
- parent = elem.parent
- if parent and parent.name == 'pre':
- lang = self._extract_language_from_classes(parent.get('class', []))
- if lang:
- return lang
-
- # Heuristic detection
- if 'import ' in code and 'from ' in code:
- return 'python'
- if 'const ' in code or 'let ' in code or '=>' in code:
- return 'javascript'
- if 'func ' in code and 'var ' in code:
- return 'gdscript'
- if 'def ' in code and ':' in code:
- return 'python'
- if '#include' in code or 'int main' in code:
- return 'cpp'
- # C# detection
- if 'using System' in code or 'namespace ' in code:
- return 'csharp'
- if '{ get; set; }' in code:
- return 'csharp'
- if any(keyword in code for keyword in ['public class ', 'private class ', 'internal class ', 'public static void ']):
- return 'csharp'
-
- return 'unknown'
-
- def extract_patterns(self, main: Any, code_samples: List[Dict[str, Any]]) -> List[Dict[str, str]]:
- """Extract common coding patterns (NEW FEATURE)"""
- patterns = []
-
- # Look for "Example:" or "Pattern:" sections
- for elem in main.find_all(['p', 'div']):
- text = elem.get_text().lower()
- if any(word in text for word in ['example:', 'pattern:', 'usage:', 'typical use']):
- # Get the code that follows
- next_code = elem.find_next(['pre', 'code'])
- if next_code:
- patterns.append({
- 'description': self.clean_text(elem.get_text()),
- 'code': next_code.get_text().strip()
- })
-
- return patterns[:5] # Limit to 5 most relevant patterns
-
- def clean_text(self, text: str) -> str:
- """Clean text content"""
- text = re.sub(r'\s+', ' ', text)
- return text.strip()
-
- def save_page(self, page: Dict[str, Any]) -> None:
- """Save page data"""
- url_hash = hashlib.md5(page['url'].encode()).hexdigest()[:10]
- safe_title = re.sub(r'[^\w\s-]', '', page['title'])[:50]
- safe_title = re.sub(r'[-\s]+', '_', safe_title)
-
- filename = f"{safe_title}_{url_hash}.json"
- filepath = os.path.join(self.data_dir, "pages", filename)
-
- with open(filepath, 'w', encoding='utf-8') as f:
- json.dump(page, f, indent=2, ensure_ascii=False)
-
- def scrape_page(self, url: str) -> None:
- """Scrape a single page with thread-safe operations.
-
- Args:
- url (str): URL to scrape
-
- Returns:
- dict or None: Page data dict on success, None on failure
-
- Note:
- Uses threading locks when workers > 1 for thread safety
- """
- try:
- # Scraping part (no lock needed - independent)
- headers = {'User-Agent': 'Mozilla/5.0 (Documentation Scraper)'}
- response = requests.get(url, headers=headers, timeout=30)
- response.raise_for_status()
-
- soup = BeautifulSoup(response.content, 'html.parser')
- page = self.extract_content(soup, url)
-
- # Thread-safe operations (lock required)
- if self.workers > 1:
- with self.lock:
- logger.info(" %s", url)
- self.save_page(page)
- self.pages.append(page)
-
- # Add new URLs
- for link in page['links']:
- if link not in self.visited_urls and link not in self.pending_urls:
- self.pending_urls.append(link)
- else:
- # Single-threaded mode (no lock needed)
- logger.info(" %s", url)
- self.save_page(page)
- self.pages.append(page)
-
- # Add new URLs
- for link in page['links']:
- if link not in self.visited_urls and link not in self.pending_urls:
- self.pending_urls.append(link)
-
- # Rate limiting
- rate_limit = self.config.get('rate_limit', DEFAULT_RATE_LIMIT)
- if rate_limit > 0:
- time.sleep(rate_limit)
-
- except Exception as e:
- if self.workers > 1:
- with self.lock:
- logger.error(" ✗ Error scraping %s: %s: %s", url, type(e).__name__, e)
- else:
- logger.error(" ✗ Error scraping page: %s: %s", type(e).__name__, e)
- logger.error(" URL: %s", url)
-
- async def scrape_page_async(self, url: str, semaphore: asyncio.Semaphore, client: httpx.AsyncClient) -> None:
- """Scrape a single page asynchronously.
-
- Args:
- url: URL to scrape
- semaphore: Asyncio semaphore for concurrency control
- client: Shared httpx AsyncClient for connection pooling
-
- Note:
- Uses asyncio.Lock for async-safe operations instead of threading.Lock
- """
- async with semaphore: # Limit concurrent requests
- try:
- # Async HTTP request
- headers = {'User-Agent': 'Mozilla/5.0 (Documentation Scraper)'}
- response = await client.get(url, headers=headers, timeout=30.0)
- response.raise_for_status()
-
- # BeautifulSoup parsing (still synchronous, but fast)
- soup = BeautifulSoup(response.content, 'html.parser')
- page = self.extract_content(soup, url)
-
- # Async-safe operations (no lock needed - single event loop)
- logger.info(" %s", url)
- self.save_page(page)
- self.pages.append(page)
-
- # Add new URLs
- for link in page['links']:
- if link not in self.visited_urls and link not in self.pending_urls:
- self.pending_urls.append(link)
-
- # Rate limiting
- rate_limit = self.config.get('rate_limit', DEFAULT_RATE_LIMIT)
- if rate_limit > 0:
- await asyncio.sleep(rate_limit)
-
- except Exception as e:
- logger.error(" ✗ Error scraping %s: %s: %s", url, type(e).__name__, e)
-
- def _try_llms_txt(self) -> bool:
- """
- Try to use llms.txt instead of HTML scraping.
- Downloads ALL available variants and stores with .md extension.
-
- Returns:
- True if llms.txt was found and processed successfully
- """
- logger.info("\n🔍 Checking for llms.txt at %s...", self.base_url)
-
- # Check for explicit config URL first
- explicit_url = self.config.get('llms_txt_url')
- if explicit_url:
- logger.info("\n📌 Using explicit llms_txt_url from config: %s", explicit_url)
-
- # Download explicit file first
- downloader = LlmsTxtDownloader(explicit_url)
- content = downloader.download()
-
- if content:
- # Save explicit file with proper .md extension
- filename = downloader.get_proper_filename()
- filepath = os.path.join(self.skill_dir, "references", filename)
- os.makedirs(os.path.dirname(filepath), exist_ok=True)
-
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(content)
- logger.info(" 💾 Saved %s (%d chars)", filename, len(content))
-
- # Also try to detect and download ALL other variants
- detector = LlmsTxtDetector(self.base_url)
- variants = detector.detect_all()
-
- if variants:
- logger.info("\n🔍 Found %d total variant(s), downloading remaining...", len(variants))
- for variant_info in variants:
- url = variant_info['url']
- variant = variant_info['variant']
-
- # Skip the explicit one we already downloaded
- if url == explicit_url:
- continue
-
- logger.info(" 📥 Downloading %s...", variant)
- extra_downloader = LlmsTxtDownloader(url)
- extra_content = extra_downloader.download()
-
- if extra_content:
- extra_filename = extra_downloader.get_proper_filename()
- extra_filepath = os.path.join(self.skill_dir, "references", extra_filename)
- with open(extra_filepath, 'w', encoding='utf-8') as f:
- f.write(extra_content)
- logger.info(" ✓ %s (%d chars)", extra_filename, len(extra_content))
-
- # Parse explicit file for skill building
- parser = LlmsTxtParser(content)
- pages = parser.parse()
-
- if pages:
- for page in pages:
- self.save_page(page)
- self.pages.append(page)
-
- self.llms_txt_detected = True
- self.llms_txt_variant = 'explicit'
- return True
-
- # Auto-detection: Find ALL variants
- detector = LlmsTxtDetector(self.base_url)
- variants = detector.detect_all()
-
- if not variants:
- logger.info("ℹ️ No llms.txt found, using HTML scraping")
- return False
-
- logger.info("✅ Found %d llms.txt variant(s)", len(variants))
-
- # Download ALL variants
- downloaded = {}
- for variant_info in variants:
- url = variant_info['url']
- variant = variant_info['variant']
-
- logger.info(" 📥 Downloading %s...", variant)
- downloader = LlmsTxtDownloader(url)
- content = downloader.download()
-
- if content:
- filename = downloader.get_proper_filename()
- downloaded[variant] = {
- 'content': content,
- 'filename': filename,
- 'size': len(content)
- }
- logger.info(" ✓ %s (%d chars)", filename, len(content))
-
- if not downloaded:
- logger.warning("⚠️ Failed to download any variants, falling back to HTML scraping")
- return False
-
- # Save ALL variants to references/
- os.makedirs(os.path.join(self.skill_dir, "references"), exist_ok=True)
-
- for variant, data in downloaded.items():
- filepath = os.path.join(self.skill_dir, "references", data['filename'])
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(data['content'])
- logger.info(" 💾 Saved %s", data['filename'])
-
- # Parse LARGEST variant for skill building
- largest = max(downloaded.items(), key=lambda x: x[1]['size'])
- logger.info("\n📄 Parsing %s for skill building...", largest[1]['filename'])
-
- parser = LlmsTxtParser(largest[1]['content'])
- pages = parser.parse()
-
- if not pages:
- logger.warning("⚠️ Failed to parse llms.txt, falling back to HTML scraping")
- return False
-
- logger.info(" ✓ Parsed %d sections", len(pages))
-
- # Save pages for skill building
- for page in pages:
- self.save_page(page)
- self.pages.append(page)
-
- self.llms_txt_detected = True
- self.llms_txt_variants = list(downloaded.keys())
-
- return True
-
- def scrape_all(self) -> None:
- """Scrape all pages (supports llms.txt and HTML scraping)
-
- Routes to async version if async_mode is enabled in config.
- """
- # Route to async version if enabled
- if self.async_mode:
- asyncio.run(self.scrape_all_async())
- return
-
- # Try llms.txt first (unless dry-run or explicitly disabled)
- if not self.dry_run and not self.skip_llms_txt:
- llms_result = self._try_llms_txt()
- if llms_result:
- logger.info("\n✅ Used llms.txt (%s) - skipping HTML scraping", self.llms_txt_variant)
- self.save_summary()
- return
-
- # HTML scraping (sync/thread-based logic)
- logger.info("\n" + "=" * 60)
- if self.dry_run:
- logger.info("DRY RUN: %s", self.name)
- else:
- logger.info("SCRAPING: %s", self.name)
- logger.info("=" * 60)
- logger.info("Base URL: %s", self.base_url)
-
- if self.dry_run:
- logger.info("Mode: Preview only (no actual scraping)\n")
- else:
- logger.info("Output: %s", self.data_dir)
- if self.workers > 1:
- logger.info("Workers: %d parallel threads", self.workers)
- logger.info("")
-
- max_pages = self.config.get('max_pages', DEFAULT_MAX_PAGES)
-
- # Handle unlimited mode
- if max_pages is None or max_pages == -1:
- logger.warning("⚠️ UNLIMITED MODE: No page limit (will scrape all pages)\n")
- unlimited = True
- else:
- unlimited = False
-
- # Dry run: preview first 20 URLs
- preview_limit = 20 if self.dry_run else max_pages
-
- # Single-threaded mode (original sequential logic)
- if self.workers <= 1:
- while self.pending_urls and (unlimited or len(self.visited_urls) < preview_limit):
- url = self.pending_urls.popleft()
-
- if url in self.visited_urls:
- continue
-
- self.visited_urls.add(url)
-
- if self.dry_run:
- # Just show what would be scraped
- logger.info(" [Preview] %s", url)
- try:
- headers = {'User-Agent': 'Mozilla/5.0 (Documentation Scraper - Dry Run)'}
- response = requests.get(url, headers=headers, timeout=10)
- soup = BeautifulSoup(response.content, 'html.parser')
-
- main_selector = self.config.get('selectors', {}).get('main_content', 'div[role="main"]')
- main = soup.select_one(main_selector)
-
- if main:
- for link in main.find_all('a', href=True):
- href = urljoin(url, link['href'])
- if self.is_valid_url(href) and href not in self.visited_urls:
- self.pending_urls.append(href)
- except Exception as e:
- # Failed to extract links in fast mode, continue anyway
- logger.warning("⚠️ Warning: Could not extract links from %s: %s", url, e)
- else:
- self.scrape_page(url)
- self.pages_scraped += 1
-
- if self.checkpoint_enabled and self.pages_scraped % self.checkpoint_interval == 0:
- self.save_checkpoint()
-
- if len(self.visited_urls) % 10 == 0:
- logger.info(" [%d pages]", len(self.visited_urls))
-
- # Multi-threaded mode (parallel scraping)
- else:
- from concurrent.futures import ThreadPoolExecutor, as_completed
-
- logger.info("🚀 Starting parallel scraping with %d workers\n", self.workers)
-
- with ThreadPoolExecutor(max_workers=self.workers) as executor:
- futures = []
-
- while self.pending_urls and (unlimited or len(self.visited_urls) < preview_limit):
- # Get next batch of URLs (thread-safe)
- batch = []
- batch_size = min(self.workers * 2, len(self.pending_urls))
-
- with self.lock:
- for _ in range(batch_size):
- if not self.pending_urls:
- break
- url = self.pending_urls.popleft()
-
- if url not in self.visited_urls:
- self.visited_urls.add(url)
- batch.append(url)
-
- # Submit batch to executor
- for url in batch:
- if unlimited or len(self.visited_urls) <= preview_limit:
- future = executor.submit(self.scrape_page, url)
- futures.append(future)
-
- # Wait for some to complete before submitting more
- completed = 0
- for future in as_completed(futures[:batch_size]):
- # Check for exceptions
- try:
- future.result() # Raises exception if scrape_page failed
- except Exception as e:
- with self.lock:
- logger.warning(" ⚠️ Worker exception: %s", e)
-
- completed += 1
-
- with self.lock:
- self.pages_scraped += 1
-
- if self.checkpoint_enabled and self.pages_scraped % self.checkpoint_interval == 0:
- self.save_checkpoint()
-
- if self.pages_scraped % 10 == 0:
- logger.info(" [%d pages scraped]", self.pages_scraped)
-
- # Remove completed futures
- futures = [f for f in futures if not f.done()]
-
- # Wait for remaining futures
- for future in as_completed(futures):
- # Check for exceptions
- try:
- future.result()
- except Exception as e:
- with self.lock:
- logger.warning(" ⚠️ Worker exception: %s", e)
-
- with self.lock:
- self.pages_scraped += 1
-
- if self.dry_run:
- logger.info("\n✅ Dry run complete: would scrape ~%d pages", len(self.visited_urls))
- if len(self.visited_urls) >= preview_limit:
- logger.info(" (showing first %d, actual scraping may find more)", preview_limit)
- logger.info("\n💡 To actually scrape, run without --dry-run")
- else:
- logger.info("\n✅ Scraped %d pages", len(self.visited_urls))
- self.save_summary()
-
- async def scrape_all_async(self) -> None:
- """Scrape all pages asynchronously (async/await version).
-
- This method provides significantly better performance for parallel scraping
- compared to thread-based scraping, with lower memory overhead and better
- CPU utilization.
-
- Performance: ~2-3x faster than sync mode with same worker count.
- """
- # Try llms.txt first (unless dry-run or explicitly disabled)
- if not self.dry_run and not self.skip_llms_txt:
- llms_result = self._try_llms_txt()
- if llms_result:
- logger.info("\n✅ Used llms.txt (%s) - skipping HTML scraping", self.llms_txt_variant)
- self.save_summary()
- return
-
- # HTML scraping (async version)
- logger.info("\n" + "=" * 60)
- if self.dry_run:
- logger.info("DRY RUN (ASYNC): %s", self.name)
- else:
- logger.info("SCRAPING (ASYNC): %s", self.name)
- logger.info("=" * 60)
- logger.info("Base URL: %s", self.base_url)
-
- if self.dry_run:
- logger.info("Mode: Preview only (no actual scraping)\n")
- else:
- logger.info("Output: %s", self.data_dir)
- logger.info("Workers: %d concurrent tasks (async)", self.workers)
- logger.info("")
-
- max_pages = self.config.get('max_pages', DEFAULT_MAX_PAGES)
-
- # Handle unlimited mode
- if max_pages is None or max_pages == -1:
- logger.warning("⚠️ UNLIMITED MODE: No page limit (will scrape all pages)\n")
- unlimited = True
- preview_limit = float('inf')
- else:
- unlimited = False
- preview_limit = 20 if self.dry_run else max_pages
-
- # Create semaphore for concurrency control
- semaphore = asyncio.Semaphore(self.workers)
-
- # Create shared HTTP client with connection pooling
- async with httpx.AsyncClient(
- timeout=30.0,
- limits=httpx.Limits(max_connections=self.workers * 2)
- ) as client:
- tasks = []
-
- while self.pending_urls and (unlimited or len(self.visited_urls) < preview_limit):
- # Get next batch of URLs
- batch = []
- batch_size = min(self.workers * 2, len(self.pending_urls))
-
- for _ in range(batch_size):
- if not self.pending_urls:
- break
- url = self.pending_urls.popleft()
-
- if url not in self.visited_urls:
- self.visited_urls.add(url)
- batch.append(url)
-
- # Create async tasks for batch
- for url in batch:
- if unlimited or len(self.visited_urls) <= preview_limit:
- if self.dry_run:
- logger.info(" [Preview] %s", url)
- else:
- task = asyncio.create_task(
- self.scrape_page_async(url, semaphore, client)
- )
- tasks.append(task)
-
- # Wait for batch to complete before continuing
- if tasks:
- await asyncio.gather(*tasks, return_exceptions=True)
- tasks = []
- self.pages_scraped = len(self.visited_urls)
-
- # Progress indicator
- if self.pages_scraped % 10 == 0 and not self.dry_run:
- logger.info(" [%d pages scraped]", self.pages_scraped)
-
- # Checkpoint saving
- if not self.dry_run and self.checkpoint_enabled:
- if self.pages_scraped % self.checkpoint_interval == 0:
- self.save_checkpoint()
-
- # Wait for any remaining tasks
- if tasks:
- await asyncio.gather(*tasks, return_exceptions=True)
-
- if self.dry_run:
- logger.info("\n✅ Dry run complete: would scrape ~%d pages", len(self.visited_urls))
- if len(self.visited_urls) >= preview_limit:
- logger.info(" (showing first %d, actual scraping may find more)", int(preview_limit))
- logger.info("\n💡 To actually scrape, run without --dry-run")
- else:
- logger.info("\n✅ Scraped %d pages (async mode)", len(self.visited_urls))
- self.save_summary()
-
- def save_summary(self) -> None:
- """Save scraping summary"""
- summary = {
- 'name': self.name,
- 'total_pages': len(self.pages),
- 'base_url': self.base_url,
- 'llms_txt_detected': self.llms_txt_detected,
- 'llms_txt_variant': self.llms_txt_variant,
- 'pages': [{'title': p['title'], 'url': p['url']} for p in self.pages]
- }
-
- with open(f"{self.data_dir}/summary.json", 'w', encoding='utf-8') as f:
- json.dump(summary, f, indent=2, ensure_ascii=False)
-
- def load_scraped_data(self) -> List[Dict[str, Any]]:
- """Load previously scraped data"""
- pages = []
- pages_dir = Path(self.data_dir) / "pages"
-
- if not pages_dir.exists():
- return []
-
- for json_file in pages_dir.glob("*.json"):
- try:
- with open(json_file, 'r', encoding='utf-8') as f:
- pages.append(json.load(f))
- except Exception as e:
- logger.error("⚠️ Error loading scraped data file %s: %s: %s", json_file, type(e).__name__, e)
- logger.error(" Suggestion: File may be corrupted, consider re-scraping with --fresh")
-
- return pages
-
- def smart_categorize(self, pages: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
- """Improved categorization with better pattern matching"""
- category_defs = self.config.get('categories', {})
-
- # Default smart categories if none provided
- if not category_defs:
- category_defs = self.infer_categories(pages)
-
- categories: Dict[str, List[Dict[str, Any]]] = {cat: [] for cat in category_defs.keys()}
- categories['other'] = []
-
- for page in pages:
- url = page['url'].lower()
- title = page['title'].lower()
- content = page.get('content', '').lower()[:CONTENT_PREVIEW_LENGTH] # Check first N chars for categorization
-
- categorized = False
-
- # Match against keywords
- for cat, keywords in category_defs.items():
- score = 0
- for keyword in keywords:
- keyword = keyword.lower()
- if keyword in url:
- score += 3
- if keyword in title:
- score += 2
- if keyword in content:
- score += 1
-
- if score >= MIN_CATEGORIZATION_SCORE: # Threshold for categorization
- categories[cat].append(page)
- categorized = True
- break
-
- if not categorized:
- categories['other'].append(page)
-
- # Remove empty categories
- categories = {k: v for k, v in categories.items() if v}
-
- return categories
-
- def infer_categories(self, pages: List[Dict[str, Any]]) -> Dict[str, List[str]]:
- """Infer categories from URL patterns (IMPROVED)"""
- url_segments: defaultdict[str, int] = defaultdict(int)
-
- for page in pages:
- path = urlparse(page['url']).path
- segments = [s for s in path.split('/') if s and s not in ['en', 'stable', 'latest', 'docs']]
-
- for seg in segments:
- url_segments[seg] += 1
-
- # Top segments become categories
- top_segments = sorted(url_segments.items(), key=lambda x: x[1], reverse=True)[:8]
-
- categories = {}
- for seg, count in top_segments:
- if count >= 3: # At least 3 pages
- categories[seg] = [seg]
-
- # Add common defaults
- if 'tutorial' not in categories and any('tutorial' in url for url in [p['url'] for p in pages]):
- categories['tutorials'] = ['tutorial', 'guide', 'getting-started']
-
- if 'api' not in categories and any('api' in url or 'reference' in url for url in [p['url'] for p in pages]):
- categories['api'] = ['api', 'reference', 'class']
-
- return categories
-
- def generate_quick_reference(self, pages: List[Dict[str, Any]]) -> List[Dict[str, str]]:
- """Generate quick reference from common patterns (NEW FEATURE)"""
- quick_ref = []
-
- # Collect all patterns
- all_patterns = []
- for page in pages:
- all_patterns.extend(page.get('patterns', []))
-
- # Get most common code patterns
- seen_codes = set()
- for pattern in all_patterns:
- code = pattern['code']
- if code not in seen_codes and len(code) < 300:
- quick_ref.append(pattern)
- seen_codes.add(code)
- if len(quick_ref) >= 15:
- break
-
- return quick_ref
-
- def create_reference_file(self, category: str, pages: List[Dict[str, Any]]) -> None:
- """Create enhanced reference file"""
- if not pages:
- return
-
- lines = []
- lines.append(f"# {self.name.title()} - {category.replace('_', ' ').title()}\n")
- lines.append(f"**Pages:** {len(pages)}\n")
- lines.append("---\n")
-
- for page in pages:
- lines.append(f"## {page['title']}\n")
- lines.append(f"**URL:** {page['url']}\n")
-
- # Table of contents from headings
- if page.get('headings'):
- lines.append("**Contents:**")
- for h in page['headings'][:10]:
- level = int(h['level'][1]) if len(h['level']) > 1 else 1
- indent = " " * max(0, level - 2)
- lines.append(f"{indent}- {h['text']}")
- lines.append("")
-
- # Content (NO TRUNCATION)
- if page.get('content'):
- lines.append(page['content'])
- lines.append("")
-
- # Code examples with language (NO TRUNCATION)
- if page.get('code_samples'):
- lines.append("**Examples:**\n")
- for i, sample in enumerate(page['code_samples'][:4], 1):
- lang = sample.get('language', 'unknown')
- code = sample.get('code', sample if isinstance(sample, str) else '')
- lines.append(f"Example {i} ({lang}):")
- lines.append(f"```{lang}")
- lines.append(code) # Full code, no truncation
- lines.append("```\n")
-
- lines.append("---\n")
-
- filepath = os.path.join(self.skill_dir, "references", f"{category}.md")
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- logger.info(" ✓ %s.md (%d pages)", category, len(pages))
-
- def create_enhanced_skill_md(self, categories: Dict[str, List[Dict[str, Any]]], quick_ref: List[Dict[str, str]]) -> None:
- """Create SKILL.md with actual examples (IMPROVED)"""
- description = self.config.get('description', f'Comprehensive assistance with {self.name}')
-
- # Extract actual code examples from docs
- example_codes = []
- for pages in categories.values():
- for page in pages[:3]: # First 3 pages per category
- for sample in page.get('code_samples', [])[:2]: # First 2 samples per page
- code = sample.get('code', sample if isinstance(sample, str) else '')
- lang = sample.get('language', 'unknown')
- if len(code) < 200 and lang != 'unknown':
- example_codes.append((lang, code))
- if len(example_codes) >= 10:
- break
- if len(example_codes) >= 10:
- break
- if len(example_codes) >= 10:
- break
-
- content = f"""---
-name: {self.name}
-description: {description}
----
-
-# {self.name.title()} Skill
-
-Comprehensive assistance with {self.name} development, generated from official documentation.
-
-## When to Use This Skill
-
-This skill should be triggered when:
-- Working with {self.name}
-- Asking about {self.name} features or APIs
-- Implementing {self.name} solutions
-- Debugging {self.name} code
-- Learning {self.name} best practices
-
-## Quick Reference
-
-### Common Patterns
-
-"""
-
- # Add actual quick reference patterns
- if quick_ref:
- for i, pattern in enumerate(quick_ref[:8], 1):
- content += f"**Pattern {i}:** {pattern.get('description', 'Example pattern')}\n\n"
- content += "```\n"
- content += pattern.get('code', '')[:300]
- content += "\n```\n\n"
- else:
- content += "*Quick reference patterns will be added as you use the skill.*\n\n"
-
- # Add example codes from docs
- if example_codes:
- content += "### Example Code Patterns\n\n"
- for i, (lang, code) in enumerate(example_codes[:5], 1):
- content += f"**Example {i}** ({lang}):\n```{lang}\n{code}\n```\n\n"
-
- content += f"""## Reference Files
-
-This skill includes comprehensive documentation in `references/`:
-
-"""
-
- for cat in sorted(categories.keys()):
- content += f"- **{cat}.md** - {cat.replace('_', ' ').title()} documentation\n"
-
- content += """
-Use `view` to read specific reference files when detailed information is needed.
-
-## Working with This Skill
-
-### For Beginners
-Start with the getting_started or tutorials reference files for foundational concepts.
-
-### For Specific Features
-Use the appropriate category reference file (api, guides, etc.) for detailed information.
-
-### For Code Examples
-The quick reference section above contains common patterns extracted from the official docs.
-
-## Resources
-
-### references/
-Organized documentation extracted from official sources. These files contain:
-- Detailed explanations
-- Code examples with language annotations
-- Links to original documentation
-- Table of contents for quick navigation
-
-### scripts/
-Add helper scripts here for common automation tasks.
-
-### assets/
-Add templates, boilerplate, or example projects here.
-
-## Notes
-
-- This skill was automatically generated from official documentation
-- Reference files preserve the structure and examples from source docs
-- Code examples include language detection for better syntax highlighting
-- Quick reference patterns are extracted from common usage examples in the docs
-
-## Updating
-
-To refresh this skill with updated documentation:
-1. Re-run the scraper with the same configuration
-2. The skill will be rebuilt with the latest information
-"""
-
- filepath = os.path.join(self.skill_dir, "SKILL.md")
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(content)
-
- logger.info(" ✓ SKILL.md (enhanced with %d examples)", len(example_codes))
-
- def create_index(self, categories: Dict[str, List[Dict[str, Any]]]) -> None:
- """Create navigation index"""
- lines = []
- lines.append(f"# {self.name.title()} Documentation Index\n")
- lines.append("## Categories\n")
-
- for cat, pages in sorted(categories.items()):
- lines.append(f"### {cat.replace('_', ' ').title()}")
- lines.append(f"**File:** `{cat}.md`")
- lines.append(f"**Pages:** {len(pages)}\n")
-
- filepath = os.path.join(self.skill_dir, "references", "index.md")
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- logger.info(" ✓ index.md")
-
- def build_skill(self) -> bool:
- """Build the skill from scraped data.
-
- Loads scraped JSON files, categorizes pages, extracts patterns,
- and generates SKILL.md and reference files.
-
- Returns:
- bool: True if build succeeded, False otherwise
- """
- logger.info("\n" + "=" * 60)
- logger.info("BUILDING SKILL: %s", self.name)
- logger.info("=" * 60 + "\n")
-
- # Load data
- logger.info("Loading scraped data...")
- pages = self.load_scraped_data()
-
- if not pages:
- logger.error("✗ No scraped data found!")
- return False
-
- logger.info(" ✓ Loaded %d pages\n", len(pages))
-
- # Categorize
- logger.info("Categorizing pages...")
- categories = self.smart_categorize(pages)
- logger.info(" ✓ Created %d categories\n", len(categories))
-
- # Generate quick reference
- logger.info("Generating quick reference...")
- quick_ref = self.generate_quick_reference(pages)
- logger.info(" ✓ Extracted %d patterns\n", len(quick_ref))
-
- # Create reference files
- logger.info("Creating reference files...")
- for cat, cat_pages in categories.items():
- self.create_reference_file(cat, cat_pages)
-
- # Create index
- self.create_index(categories)
- logger.info("")
-
- # Create enhanced SKILL.md
- logger.info("Creating SKILL.md...")
- self.create_enhanced_skill_md(categories, quick_ref)
-
- logger.info("\n✅ Skill built: %s/", self.skill_dir)
- return True
-
-
-def validate_config(config: Dict[str, Any]) -> Tuple[List[str], List[str]]:
- """Validate configuration structure and values.
-
- Args:
- config (dict): Configuration dictionary to validate
-
- Returns:
- tuple: (errors, warnings) where each is a list of strings
-
- Example:
- >>> errors, warnings = validate_config({'name': 'test', 'base_url': 'https://example.com'})
- >>> if errors:
- ... print("Invalid config:", errors)
- """
- errors = []
- warnings = []
-
- # Required fields
- required_fields = ['name', 'base_url']
- for field in required_fields:
- if field not in config:
- errors.append(f"Missing required field: '{field}'")
-
- # Validate name (alphanumeric, hyphens, underscores only)
- if 'name' in config:
- if not re.match(r'^[a-zA-Z0-9_-]+$', config['name']):
- errors.append(f"Invalid name: '{config['name']}' (use only letters, numbers, hyphens, underscores)")
-
- # Validate base_url
- if 'base_url' in config:
- if not config['base_url'].startswith(('http://', 'https://')):
- errors.append(f"Invalid base_url: '{config['base_url']}' (must start with http:// or https://)")
-
- # Validate selectors structure
- if 'selectors' in config:
- if not isinstance(config['selectors'], dict):
- errors.append("'selectors' must be a dictionary")
- else:
- recommended_selectors = ['main_content', 'title', 'code_blocks']
- for selector in recommended_selectors:
- if selector not in config['selectors']:
- warnings.append(f"Missing recommended selector: '{selector}'")
- else:
- warnings.append("Missing 'selectors' section (recommended)")
-
- # Validate url_patterns
- if 'url_patterns' in config:
- if not isinstance(config['url_patterns'], dict):
- errors.append("'url_patterns' must be a dictionary")
- else:
- for key in ['include', 'exclude']:
- if key in config['url_patterns']:
- if not isinstance(config['url_patterns'][key], list):
- errors.append(f"'url_patterns.{key}' must be a list")
-
- # Validate categories
- if 'categories' in config:
- if not isinstance(config['categories'], dict):
- errors.append("'categories' must be a dictionary")
- else:
- for cat_name, keywords in config['categories'].items():
- if not isinstance(keywords, list):
- errors.append(f"'categories.{cat_name}' must be a list of keywords")
-
- # Validate rate_limit
- if 'rate_limit' in config:
- try:
- rate = float(config['rate_limit'])
- if rate < 0:
- errors.append(f"'rate_limit' must be non-negative (got {rate})")
- elif rate > 10:
- warnings.append(f"'rate_limit' is very high ({rate}s) - this may slow down scraping significantly")
- except (ValueError, TypeError):
- errors.append(f"'rate_limit' must be a number (got {config['rate_limit']})")
-
- # Validate max_pages
- if 'max_pages' in config:
- max_p_value = config['max_pages']
-
- # Allow None for unlimited
- if max_p_value is None:
- warnings.append("'max_pages' is None (unlimited) - this will scrape ALL pages. Use with caution!")
- else:
- try:
- max_p = int(max_p_value)
- # Allow -1 for unlimited
- if max_p == -1:
- warnings.append("'max_pages' is -1 (unlimited) - this will scrape ALL pages. Use with caution!")
- elif max_p < 1:
- errors.append(f"'max_pages' must be at least 1 or -1 for unlimited (got {max_p})")
- elif max_p > MAX_PAGES_WARNING_THRESHOLD:
- warnings.append(f"'max_pages' is very high ({max_p}) - scraping may take a very long time")
- except (ValueError, TypeError):
- errors.append(f"'max_pages' must be an integer, -1, or null (got {config['max_pages']})")
-
- # Validate start_urls if present
- if 'start_urls' in config:
- if not isinstance(config['start_urls'], list):
- errors.append("'start_urls' must be a list")
- else:
- for url in config['start_urls']:
- if not url.startswith(('http://', 'https://')):
- errors.append(f"Invalid start_url: '{url}' (must start with http:// or https://)")
-
- return errors, warnings
-
-
-def load_config(config_path: str) -> Dict[str, Any]:
- """Load and validate configuration from JSON file.
-
- Args:
- config_path (str): Path to JSON configuration file
-
- Returns:
- dict: Validated configuration dictionary
-
- Raises:
- SystemExit: If config is invalid or file not found
-
- Example:
- >>> config = load_config('configs/react.json')
- >>> print(config['name'])
- 'react'
- """
- try:
- with open(config_path, 'r') as f:
- config = json.load(f)
- except json.JSONDecodeError as e:
- logger.error("❌ Error: Invalid JSON in config file: %s", config_path)
- logger.error(" Details: %s", e)
- logger.error(" Suggestion: Check syntax at line %d, column %d", e.lineno, e.colno)
- sys.exit(1)
- except FileNotFoundError:
- logger.error("❌ Error: Config file not found: %s", config_path)
- logger.error(" Suggestion: Create a config file or use an existing one from configs/")
- logger.error(" Available configs: react.json, vue.json, django.json, godot.json")
- sys.exit(1)
-
- # Validate config
- errors, warnings = validate_config(config)
-
- # Show warnings (non-blocking)
- if warnings:
- logger.warning("⚠️ Configuration warnings in %s:", config_path)
- for warning in warnings:
- logger.warning(" - %s", warning)
- logger.info("")
-
- # Show errors (blocking)
- if errors:
- logger.error("❌ Configuration validation errors in %s:", config_path)
- for error in errors:
- logger.error(" - %s", error)
- logger.error("\n Suggestion: Fix the above errors or check configs/ for working examples")
- sys.exit(1)
-
- return config
-
-
-def interactive_config() -> Dict[str, Any]:
- """Interactive configuration wizard for creating new configs.
-
- Prompts user for all required configuration fields step-by-step
- and returns a complete configuration dictionary.
-
- Returns:
- dict: Complete configuration dictionary with user-provided values
-
- Example:
- >>> config = interactive_config()
- # User enters: name=react, url=https://react.dev, etc.
- >>> config['name']
- 'react'
- """
- logger.info("\n" + "="*60)
- logger.info("Documentation to Skill Converter")
- logger.info("="*60 + "\n")
-
- config: Dict[str, Any] = {}
-
- # Basic info
- config['name'] = input("Skill name (e.g., 'react', 'godot'): ").strip()
- config['description'] = input("Skill description: ").strip()
- config['base_url'] = input("Base URL (e.g., https://docs.example.com/): ").strip()
-
- if not config['base_url'].endswith('/'):
- config['base_url'] += '/'
-
- # Selectors
- logger.info("\nCSS Selectors (press Enter for defaults):")
- selectors = {}
- selectors['main_content'] = input(" Main content [div[role='main']]: ").strip() or "div[role='main']"
- selectors['title'] = input(" Title [title]: ").strip() or "title"
- selectors['code_blocks'] = input(" Code blocks [pre code]: ").strip() or "pre code"
- config['selectors'] = selectors
-
- # URL patterns
- logger.info("\nURL Patterns (comma-separated, optional):")
- include = input(" Include: ").strip()
- exclude = input(" Exclude: ").strip()
- config['url_patterns'] = {
- 'include': [p.strip() for p in include.split(',') if p.strip()],
- 'exclude': [p.strip() for p in exclude.split(',') if p.strip()]
- }
-
- # Settings
- rate = input(f"\nRate limit (seconds) [{DEFAULT_RATE_LIMIT}]: ").strip()
- config['rate_limit'] = float(rate) if rate else DEFAULT_RATE_LIMIT
-
- max_p = input(f"Max pages [{DEFAULT_MAX_PAGES}]: ").strip()
- config['max_pages'] = int(max_p) if max_p else DEFAULT_MAX_PAGES
-
- return config
-
-
-def check_existing_data(name: str) -> Tuple[bool, int]:
- """Check if scraped data already exists for a skill.
-
- Args:
- name (str): Skill name to check
-
- Returns:
- tuple: (exists, page_count) where exists is bool and page_count is int
-
- Example:
- >>> exists, count = check_existing_data('react')
- >>> if exists:
- ... print(f"Found {count} existing pages")
- """
- data_dir = f"output/{name}_data"
- if os.path.exists(data_dir) and os.path.exists(f"{data_dir}/summary.json"):
- with open(f"{data_dir}/summary.json", 'r') as f:
- summary = json.load(f)
- return True, summary.get('total_pages', 0)
- return False, 0
-
-
-def setup_argument_parser() -> argparse.ArgumentParser:
- """Setup and configure command-line argument parser.
-
- Creates an ArgumentParser with all CLI options for the doc scraper tool,
- including configuration, scraping, enhancement, and performance options.
-
- Returns:
- argparse.ArgumentParser: Configured argument parser
-
- Example:
- >>> parser = setup_argument_parser()
- >>> args = parser.parse_args(['--config', 'configs/react.json'])
- >>> print(args.config)
- configs/react.json
- """
- parser = argparse.ArgumentParser(
- description='Convert documentation websites to Claude skills',
- formatter_class=argparse.RawDescriptionHelpFormatter
- )
-
- parser.add_argument('--interactive', '-i', action='store_true',
- help='Interactive configuration mode')
- parser.add_argument('--config', '-c', type=str,
- help='Load configuration from file (e.g., configs/godot.json)')
- parser.add_argument('--name', type=str,
- help='Skill name')
- parser.add_argument('--url', type=str,
- help='Base documentation URL')
- parser.add_argument('--description', '-d', type=str,
- help='Skill description')
- parser.add_argument('--skip-scrape', action='store_true',
- help='Skip scraping, use existing data')
- parser.add_argument('--dry-run', action='store_true',
- help='Preview what will be scraped without actually scraping')
- parser.add_argument('--enhance', action='store_true',
- help='Enhance SKILL.md using Claude API after building (requires API key)')
- parser.add_argument('--enhance-local', action='store_true',
- help='Enhance SKILL.md using Claude Code (no API key needed, runs in background)')
- parser.add_argument('--interactive-enhancement', action='store_true',
- help='Open terminal window for enhancement (use with --enhance-local)')
- parser.add_argument('--api-key', type=str,
- help='Anthropic API key for --enhance (or set ANTHROPIC_API_KEY)')
- parser.add_argument('--resume', action='store_true',
- help='Resume from last checkpoint (for interrupted scrapes)')
- parser.add_argument('--fresh', action='store_true',
- help='Clear checkpoint and start fresh')
- parser.add_argument('--rate-limit', '-r', type=float, metavar='SECONDS',
- help=f'Override rate limit in seconds (default: from config or {DEFAULT_RATE_LIMIT}). Use 0 for no delay.')
- parser.add_argument('--workers', '-w', type=int, metavar='N',
- help='Number of parallel workers for faster scraping (default: 1, max: 10)')
- parser.add_argument('--async', dest='async_mode', action='store_true',
- help='Enable async mode for better parallel performance (2-3x faster than threads)')
- parser.add_argument('--no-rate-limit', action='store_true',
- help='Disable rate limiting completely (same as --rate-limit 0)')
- parser.add_argument('--verbose', '-v', action='store_true',
- help='Enable verbose output (DEBUG level logging)')
- parser.add_argument('--quiet', '-q', action='store_true',
- help='Minimize output (WARNING level logging only)')
-
- return parser
-
-
-def get_configuration(args: argparse.Namespace) -> Dict[str, Any]:
- """Load or create configuration from command-line arguments.
-
- Handles three configuration modes:
- 1. Load from JSON file (--config)
- 2. Interactive configuration wizard (--interactive or missing args)
- 3. Quick mode from command-line arguments (--name, --url)
-
- Also applies CLI overrides for rate limiting and worker count.
-
- Args:
- args: Parsed command-line arguments from argparse
-
- Returns:
- dict: Configuration dictionary with all required fields
-
- Example:
- >>> args = parser.parse_args(['--name', 'react', '--url', 'https://react.dev'])
- >>> config = get_configuration(args)
- >>> print(config['name'])
- react
- """
- # Get base configuration
- if args.config:
- config = load_config(args.config)
- elif args.interactive or not (args.name and args.url):
- config = interactive_config()
- else:
- config = {
- 'name': args.name,
- 'description': args.description or f'Comprehensive assistance with {args.name}',
- 'base_url': args.url,
- 'selectors': {
- 'main_content': "div[role='main']",
- 'title': 'title',
- 'code_blocks': 'pre code'
- },
- 'url_patterns': {'include': [], 'exclude': []},
- 'rate_limit': DEFAULT_RATE_LIMIT,
- 'max_pages': DEFAULT_MAX_PAGES
- }
-
- # Apply CLI overrides for rate limiting
- if args.no_rate_limit:
- config['rate_limit'] = 0
- logger.info("⚡ Rate limiting disabled")
- elif args.rate_limit is not None:
- config['rate_limit'] = args.rate_limit
- if args.rate_limit == 0:
- logger.info("⚡ Rate limiting disabled")
- else:
- logger.info("⚡ Rate limit override: %ss per page", args.rate_limit)
-
- # Apply CLI overrides for worker count
- if args.workers:
- # Validate workers count
- if args.workers < 1:
- logger.error("❌ Error: --workers must be at least 1 (got %d)", args.workers)
- logger.error(" Suggestion: Use --workers 1 (default) or omit the flag")
- sys.exit(1)
- if args.workers > 10:
- logger.warning("⚠️ Warning: --workers capped at 10 (requested %d)", args.workers)
- args.workers = 10
- config['workers'] = args.workers
- if args.workers > 1:
- logger.info("🚀 Parallel scraping enabled: %d workers", args.workers)
-
- # Apply CLI override for async mode
- if args.async_mode:
- config['async_mode'] = True
- if config.get('workers', 1) > 1:
- logger.info("⚡ Async mode enabled (2-3x faster than threads)")
- else:
- logger.warning("⚠️ Async mode enabled but workers=1. Consider using --workers 4 for better performance")
-
- return config
-
-
-def execute_scraping_and_building(config: Dict[str, Any], args: argparse.Namespace) -> Optional['DocToSkillConverter']:
- """Execute the scraping and skill building process.
-
- Handles dry run mode, existing data checks, scraping with checkpoints,
- keyboard interrupts, and skill building. This is the core workflow
- orchestration for the scraping phase.
-
- Args:
- config (dict): Configuration dictionary with scraping parameters
- args: Parsed command-line arguments
-
- Returns:
- DocToSkillConverter: The converter instance after scraping/building,
- or None if process was aborted
-
- Example:
- >>> config = {'name': 'react', 'base_url': 'https://react.dev'}
- >>> converter = execute_scraping_and_building(config, args)
- >>> if converter:
- ... print("Scraping complete!")
- """
- # Dry run mode - preview only
- if args.dry_run:
- logger.info("\n" + "=" * 60)
- logger.info("DRY RUN MODE")
- logger.info("=" * 60)
- logger.info("This will show what would be scraped without saving anything.\n")
-
- converter = DocToSkillConverter(config, dry_run=True)
- converter.scrape_all()
-
- logger.info("\n📋 Configuration Summary:")
- logger.info(" Name: %s", config['name'])
- logger.info(" Base URL: %s", config['base_url'])
- logger.info(" Max pages: %d", config.get('max_pages', DEFAULT_MAX_PAGES))
- logger.info(" Rate limit: %ss", config.get('rate_limit', DEFAULT_RATE_LIMIT))
- logger.info(" Categories: %d", len(config.get('categories', {})))
- return None
-
- # Check for existing data
- exists, page_count = check_existing_data(config['name'])
-
- if exists and not args.skip_scrape and not args.fresh:
- # Check force_rescrape flag from config
- if config.get('force_rescrape', False):
- # Auto-delete cached data and rescrape
- logger.info("\n✓ Found existing data: %d pages", page_count)
- logger.info(" force_rescrape enabled - deleting cached data and rescaping")
- import shutil
- data_dir = f"output/{config['name']}_data"
- if os.path.exists(data_dir):
- shutil.rmtree(data_dir)
- logger.info(f" Deleted: {data_dir}")
- else:
- # Only prompt if force_rescrape is False
- logger.info("\n✓ Found existing data: %d pages", page_count)
- response = input("Use existing data? (y/n): ").strip().lower()
- if response == 'y':
- args.skip_scrape = True
- elif exists and args.fresh:
- logger.info("\n✓ Found existing data: %d pages", page_count)
- logger.info(" --fresh flag set, will re-scrape from scratch")
-
- # Create converter
- converter = DocToSkillConverter(config, resume=args.resume)
-
- # Handle fresh start (clear checkpoint)
- if args.fresh:
- converter.clear_checkpoint()
-
- # Scrape or skip
- if not args.skip_scrape:
- try:
- converter.scrape_all()
- # Save final checkpoint
- if converter.checkpoint_enabled:
- converter.save_checkpoint()
- logger.info("\n💾 Final checkpoint saved")
- # Clear checkpoint after successful completion
- converter.clear_checkpoint()
- logger.info("✅ Scraping complete - checkpoint cleared")
- except KeyboardInterrupt:
- logger.warning("\n\nScraping interrupted.")
- if converter.checkpoint_enabled:
- converter.save_checkpoint()
- logger.info("💾 Progress saved to checkpoint")
- logger.info(" Resume with: --config %s --resume", args.config if args.config else 'config.json')
- response = input("Continue with skill building? (y/n): ").strip().lower()
- if response != 'y':
- return None
- else:
- logger.info("\n⏭️ Skipping scrape, using existing data")
-
- # Build skill
- success = converter.build_skill()
-
- if not success:
- sys.exit(1)
-
- return converter
-
-
-def execute_enhancement(config: Dict[str, Any], args: argparse.Namespace) -> None:
- """Execute optional SKILL.md enhancement with Claude.
-
- Supports two enhancement modes:
- 1. API-based enhancement (requires ANTHROPIC_API_KEY)
- 2. Local enhancement using Claude Code (no API key needed)
-
- Prints appropriate messages and suggestions based on whether
- enhancement was requested and whether it succeeded.
-
- Args:
- config (dict): Configuration dictionary with skill name
- args: Parsed command-line arguments with enhancement flags
-
- Example:
- >>> execute_enhancement(config, args)
- # Runs enhancement if --enhance or --enhance-local flag is set
- """
- import subprocess
-
- # Optional enhancement with Claude API
- if args.enhance:
- logger.info("\n" + "=" * 60)
- logger.info("ENHANCING SKILL.MD WITH CLAUDE API")
- logger.info("=" * 60 + "\n")
-
- try:
- enhance_cmd = ['python3', 'cli/enhance_skill.py', f'output/{config["name"]}/']
- if args.api_key:
- enhance_cmd.extend(['--api-key', args.api_key])
-
- result = subprocess.run(enhance_cmd, check=True)
- if result.returncode == 0:
- logger.info("\n✅ Enhancement complete!")
- except subprocess.CalledProcessError:
- logger.warning("\n⚠ Enhancement failed, but skill was still built")
- except FileNotFoundError:
- logger.warning("\n⚠ enhance_skill.py not found. Run manually:")
- logger.info(" skill-seekers-enhance output/%s/", config['name'])
-
- # Optional enhancement with Claude Code (local, no API key)
- if args.enhance_local:
- logger.info("\n" + "=" * 60)
- if args.interactive_enhancement:
- logger.info("ENHANCING SKILL.MD WITH CLAUDE CODE (INTERACTIVE)")
- else:
- logger.info("ENHANCING SKILL.MD WITH CLAUDE CODE (HEADLESS)")
- logger.info("=" * 60 + "\n")
-
- try:
- enhance_cmd = ['skill-seekers-enhance', f'output/{config["name"]}/']
- if args.interactive_enhancement:
- enhance_cmd.append('--interactive-enhancement')
-
- result = subprocess.run(enhance_cmd, check=True)
-
- if result.returncode == 0:
- logger.info("\n✅ Enhancement complete!")
- except subprocess.CalledProcessError:
- logger.warning("\n⚠ Enhancement failed, but skill was still built")
- except FileNotFoundError:
- logger.warning("\n⚠ skill-seekers-enhance command not found. Run manually:")
- logger.info(" skill-seekers-enhance output/%s/", config['name'])
-
- # Print packaging instructions
- logger.info("\n📦 Package your skill:")
- logger.info(" skill-seekers-package output/%s/", config['name'])
-
- # Suggest enhancement if not done
- if not args.enhance and not args.enhance_local:
- logger.info("\n💡 Optional: Enhance SKILL.md with Claude:")
- logger.info(" Local (recommended): skill-seekers-enhance output/%s/", config['name'])
- logger.info(" or re-run with: --enhance-local")
- logger.info(" API-based: skill-seekers-enhance-api output/%s/", config['name'])
- logger.info(" or re-run with: --enhance")
- logger.info("\n💡 Tip: Use --interactive-enhancement with --enhance-local to open terminal window")
-
-
-def main() -> None:
- parser = setup_argument_parser()
- args = parser.parse_args()
-
- # Setup logging based on verbosity flags
- setup_logging(verbose=args.verbose, quiet=args.quiet)
-
- config = get_configuration(args)
-
- # Execute scraping and building
- converter = execute_scraping_and_building(config, args)
-
- # Exit if dry run or aborted
- if converter is None:
- return
-
- # Execute enhancement and print instructions
- execute_enhancement(config, args)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill.py
deleted file mode 100644
index 50df45b..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill.py
+++ /dev/null
@@ -1,273 +0,0 @@
-#!/usr/bin/env python3
-"""
-SKILL.md Enhancement Script
-Uses Claude API to improve SKILL.md by analyzing reference documentation.
-
-Usage:
- skill-seekers enhance output/steam-inventory/
- skill-seekers enhance output/react/
- skill-seekers enhance output/godot/ --api-key YOUR_API_KEY
-"""
-
-import os
-import sys
-import json
-import argparse
-from pathlib import Path
-
-# Add parent directory to path for imports when run as script
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.constants import API_CONTENT_LIMIT, API_PREVIEW_LIMIT
-from skill_seekers.cli.utils import read_reference_files
-
-try:
- import anthropic
-except ImportError:
- print("❌ Error: anthropic package not installed")
- print("Install with: pip3 install anthropic")
- sys.exit(1)
-
-
-class SkillEnhancer:
- def __init__(self, skill_dir, api_key=None):
- self.skill_dir = Path(skill_dir)
- self.references_dir = self.skill_dir / "references"
- self.skill_md_path = self.skill_dir / "SKILL.md"
-
- # Get API key
- self.api_key = api_key or os.environ.get('ANTHROPIC_API_KEY')
- if not self.api_key:
- raise ValueError(
- "No API key provided. Set ANTHROPIC_API_KEY environment variable "
- "or use --api-key argument"
- )
-
- self.client = anthropic.Anthropic(api_key=self.api_key)
-
- def read_current_skill_md(self):
- """Read existing SKILL.md"""
- if not self.skill_md_path.exists():
- return None
- return self.skill_md_path.read_text(encoding='utf-8')
-
- def enhance_skill_md(self, references, current_skill_md):
- """Use Claude to enhance SKILL.md"""
-
- # Build prompt
- prompt = self._build_enhancement_prompt(references, current_skill_md)
-
- print("\n🤖 Asking Claude to enhance SKILL.md...")
- print(f" Input: {len(prompt):,} characters")
-
- try:
- message = self.client.messages.create(
- model="claude-sonnet-4-20250514",
- max_tokens=4096,
- temperature=0.3,
- messages=[{
- "role": "user",
- "content": prompt
- }]
- )
-
- enhanced_content = message.content[0].text
- return enhanced_content
-
- except Exception as e:
- print(f"❌ Error calling Claude API: {e}")
- return None
-
- def _build_enhancement_prompt(self, references, current_skill_md):
- """Build the prompt for Claude"""
-
- # Extract skill name and description
- skill_name = self.skill_dir.name
-
- prompt = f"""You are enhancing a Claude skill's SKILL.md file. This skill is about: {skill_name}
-
-I've scraped documentation and organized it into reference files. Your job is to create an EXCELLENT SKILL.md that will help Claude use this documentation effectively.
-
-CURRENT SKILL.MD:
-{'```markdown' if current_skill_md else '(none - create from scratch)'}
-{current_skill_md or 'No existing SKILL.md'}
-{'```' if current_skill_md else ''}
-
-REFERENCE DOCUMENTATION:
-"""
-
- for filename, content in references.items():
- prompt += f"\n\n## {filename}\n```markdown\n{content[:30000]}\n```\n"
-
- prompt += """
-
-YOUR TASK:
-Create an enhanced SKILL.md that includes:
-
-1. **Clear "When to Use This Skill" section** - Be specific about trigger conditions
-2. **Excellent Quick Reference section** - Extract 5-10 of the BEST, most practical code examples from the reference docs
- - Choose SHORT, clear examples that demonstrate common tasks
- - Include both simple and intermediate examples
- - Annotate examples with clear descriptions
- - Use proper language tags (cpp, python, javascript, json, etc.)
-3. **Detailed Reference Files description** - Explain what's in each reference file
-4. **Practical "Working with This Skill" section** - Give users clear guidance on how to navigate the skill
-5. **Key Concepts section** (if applicable) - Explain core concepts
-6. **Keep the frontmatter** (---\nname: ...\n---) intact
-
-IMPORTANT:
-- Extract REAL examples from the reference docs, don't make them up
-- Prioritize SHORT, clear examples (5-20 lines max)
-- Make it actionable and practical
-- Don't be too verbose - be concise but useful
-- Maintain the markdown structure for Claude skills
-- Keep code examples properly formatted with language tags
-
-OUTPUT:
-Return ONLY the complete SKILL.md content, starting with the frontmatter (---).
-"""
-
- return prompt
-
- def save_enhanced_skill_md(self, content):
- """Save the enhanced SKILL.md"""
- # Backup original
- if self.skill_md_path.exists():
- backup_path = self.skill_md_path.with_suffix('.md.backup')
- self.skill_md_path.rename(backup_path)
- print(f" 💾 Backed up original to: {backup_path.name}")
-
- # Save enhanced version
- self.skill_md_path.write_text(content, encoding='utf-8')
- print(f" ✅ Saved enhanced SKILL.md")
-
- def run(self):
- """Main enhancement workflow"""
- print(f"\n{'='*60}")
- print(f"ENHANCING SKILL: {self.skill_dir.name}")
- print(f"{'='*60}\n")
-
- # Read reference files
- print("📖 Reading reference documentation...")
- references = read_reference_files(
- self.skill_dir,
- max_chars=API_CONTENT_LIMIT,
- preview_limit=API_PREVIEW_LIMIT
- )
-
- if not references:
- print("❌ No reference files found to analyze")
- return False
-
- print(f" ✓ Read {len(references)} reference files")
- total_size = sum(len(c) for c in references.values())
- print(f" ✓ Total size: {total_size:,} characters\n")
-
- # Read current SKILL.md
- current_skill_md = self.read_current_skill_md()
- if current_skill_md:
- print(f" ℹ Found existing SKILL.md ({len(current_skill_md)} chars)")
- else:
- print(f" ℹ No existing SKILL.md, will create new one")
-
- # Enhance with Claude
- enhanced = self.enhance_skill_md(references, current_skill_md)
-
- if not enhanced:
- print("❌ Enhancement failed")
- return False
-
- print(f" ✓ Generated enhanced SKILL.md ({len(enhanced)} chars)\n")
-
- # Save
- print("💾 Saving enhanced SKILL.md...")
- self.save_enhanced_skill_md(enhanced)
-
- print(f"\n✅ Enhancement complete!")
- print(f"\nNext steps:")
- print(f" 1. Review: {self.skill_md_path}")
- print(f" 2. If you don't like it, restore backup: {self.skill_md_path.with_suffix('.md.backup')}")
- print(f" 3. Package your skill:")
- print(f" skill-seekers package {self.skill_dir}/")
-
- return True
-
-
-def main():
- parser = argparse.ArgumentParser(
- description='Enhance SKILL.md using Claude API',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Using ANTHROPIC_API_KEY environment variable
- export ANTHROPIC_API_KEY=sk-ant-...
- skill-seekers enhance output/steam-inventory/
-
- # Providing API key directly
- skill-seekers enhance output/react/ --api-key sk-ant-...
-
- # Show what would be done (dry run)
- skill-seekers enhance output/godot/ --dry-run
-"""
- )
-
- parser.add_argument('skill_dir', type=str,
- help='Path to skill directory (e.g., output/steam-inventory/)')
- parser.add_argument('--api-key', type=str,
- help='Anthropic API key (or set ANTHROPIC_API_KEY env var)')
- parser.add_argument('--dry-run', action='store_true',
- help='Show what would be done without calling API')
-
- args = parser.parse_args()
-
- # Validate skill directory
- skill_dir = Path(args.skill_dir)
- if not skill_dir.exists():
- print(f"❌ Error: Directory not found: {skill_dir}")
- sys.exit(1)
-
- if not skill_dir.is_dir():
- print(f"❌ Error: Not a directory: {skill_dir}")
- sys.exit(1)
-
- # Dry run mode
- if args.dry_run:
- print(f"🔍 DRY RUN MODE")
- print(f" Would enhance: {skill_dir}")
- print(f" References: {skill_dir / 'references'}")
- print(f" SKILL.md: {skill_dir / 'SKILL.md'}")
-
- refs_dir = skill_dir / "references"
- if refs_dir.exists():
- ref_files = list(refs_dir.glob("*.md"))
- print(f" Found {len(ref_files)} reference files:")
- for rf in ref_files:
- size = rf.stat().st_size
- print(f" - {rf.name} ({size:,} bytes)")
-
- print("\nTo actually run enhancement:")
- print(f" skill-seekers enhance {skill_dir}")
- return
-
- # Create enhancer and run
- try:
- enhancer = SkillEnhancer(skill_dir, api_key=args.api_key)
- success = enhancer.run()
- sys.exit(0 if success else 1)
-
- except ValueError as e:
- print(f"❌ Error: {e}")
- print("\nSet your API key:")
- print(" export ANTHROPIC_API_KEY=sk-ant-...")
- print("Or provide it directly:")
- print(f" skill-seekers enhance {skill_dir} --api-key sk-ant-...")
- sys.exit(1)
- except Exception as e:
- print(f"❌ Unexpected error: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill_local.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill_local.py
deleted file mode 100644
index 99480c5..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill_local.py
+++ /dev/null
@@ -1,451 +0,0 @@
-#!/usr/bin/env python3
-"""
-SKILL.md Enhancement Script (Local - Using Claude Code)
-Opens a new terminal with Claude Code to enhance SKILL.md, then reports back.
-No API key needed - uses your existing Claude Code Max plan!
-
-Usage:
- skill-seekers enhance output/steam-inventory/
- skill-seekers enhance output/react/
-
-Terminal Selection:
- The script automatically detects which terminal app to use:
- 1. SKILL_SEEKER_TERMINAL env var (highest priority)
- Example: export SKILL_SEEKER_TERMINAL="Ghostty"
- 2. TERM_PROGRAM env var (current terminal)
- 3. Terminal.app (fallback)
-
- Supported terminals: Ghostty, iTerm, Terminal, WezTerm
-"""
-
-import os
-import sys
-import time
-import subprocess
-import tempfile
-from pathlib import Path
-
-# Add parent directory to path for imports when run as script
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.constants import LOCAL_CONTENT_LIMIT, LOCAL_PREVIEW_LIMIT
-from skill_seekers.cli.utils import read_reference_files
-
-
-def detect_terminal_app():
- """Detect which terminal app to use with cascading priority.
-
- Priority order:
- 1. SKILL_SEEKER_TERMINAL environment variable (explicit user preference)
- 2. TERM_PROGRAM environment variable (inherit current terminal)
- 3. Terminal.app (fallback default)
-
- Returns:
- tuple: (terminal_app_name, detection_method)
- - terminal_app_name (str): Name of terminal app to launch (e.g., "Ghostty", "Terminal")
- - detection_method (str): How the terminal was detected (for logging)
-
- Examples:
- >>> os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
- >>> detect_terminal_app()
- ('Ghostty', 'SKILL_SEEKER_TERMINAL')
-
- >>> os.environ['TERM_PROGRAM'] = 'iTerm.app'
- >>> detect_terminal_app()
- ('iTerm', 'TERM_PROGRAM')
- """
- # Map TERM_PROGRAM values to macOS app names
- TERMINAL_MAP = {
- 'Apple_Terminal': 'Terminal',
- 'iTerm.app': 'iTerm',
- 'ghostty': 'Ghostty',
- 'WezTerm': 'WezTerm',
- }
-
- # Priority 1: Check SKILL_SEEKER_TERMINAL env var (explicit preference)
- preferred_terminal = os.environ.get('SKILL_SEEKER_TERMINAL', '').strip()
- if preferred_terminal:
- return preferred_terminal, 'SKILL_SEEKER_TERMINAL'
-
- # Priority 2: Check TERM_PROGRAM (inherit current terminal)
- term_program = os.environ.get('TERM_PROGRAM', '').strip()
- if term_program and term_program in TERMINAL_MAP:
- return TERMINAL_MAP[term_program], 'TERM_PROGRAM'
-
- # Priority 3: Fallback to Terminal.app
- if term_program:
- # TERM_PROGRAM is set but unknown
- return 'Terminal', f'unknown TERM_PROGRAM ({term_program})'
- else:
- # No TERM_PROGRAM set
- return 'Terminal', 'default'
-
-
-class LocalSkillEnhancer:
- def __init__(self, skill_dir):
- self.skill_dir = Path(skill_dir)
- self.references_dir = self.skill_dir / "references"
- self.skill_md_path = self.skill_dir / "SKILL.md"
-
- def create_enhancement_prompt(self):
- """Create the prompt file for Claude Code"""
-
- # Read reference files
- references = read_reference_files(
- self.skill_dir,
- max_chars=LOCAL_CONTENT_LIMIT,
- preview_limit=LOCAL_PREVIEW_LIMIT
- )
-
- if not references:
- print("❌ No reference files found")
- return None
-
- # Read current SKILL.md
- current_skill_md = ""
- if self.skill_md_path.exists():
- current_skill_md = self.skill_md_path.read_text(encoding='utf-8')
-
- # Build prompt
- prompt = f"""I need you to enhance the SKILL.md file for the {self.skill_dir.name} skill.
-
-CURRENT SKILL.MD:
-{'-'*60}
-{current_skill_md if current_skill_md else '(No existing SKILL.md - create from scratch)'}
-{'-'*60}
-
-REFERENCE DOCUMENTATION:
-{'-'*60}
-"""
-
- for filename, content in references.items():
- prompt += f"\n## {filename}\n{content[:15000]}\n"
-
- prompt += f"""
-{'-'*60}
-
-YOUR TASK:
-Create an EXCELLENT SKILL.md file that will help Claude use this documentation effectively.
-
-Requirements:
-1. **Clear "When to Use This Skill" section**
- - Be SPECIFIC about trigger conditions
- - List concrete use cases
-
-2. **Excellent Quick Reference section**
- - Extract 5-10 of the BEST, most practical code examples from the reference docs
- - Choose SHORT, clear examples (5-20 lines max)
- - Include both simple and intermediate examples
- - Use proper language tags (cpp, python, javascript, json, etc.)
- - Add clear descriptions for each example
-
-3. **Detailed Reference Files description**
- - Explain what's in each reference file
- - Help users navigate the documentation
-
-4. **Practical "Working with This Skill" section**
- - Clear guidance for beginners, intermediate, and advanced users
- - Navigation tips
-
-5. **Key Concepts section** (if applicable)
- - Explain core concepts
- - Define important terminology
-
-IMPORTANT:
-- Extract REAL examples from the reference docs above
-- Prioritize SHORT, clear examples
-- Make it actionable and practical
-- Keep the frontmatter (---\\nname: ...\\n---) intact
-- Use proper markdown formatting
-
-SAVE THE RESULT:
-Save the complete enhanced SKILL.md to: {self.skill_md_path.absolute()}
-
-First, backup the original to: {self.skill_md_path.with_suffix('.md.backup').absolute()}
-"""
-
- return prompt
-
- def run(self, headless=True, timeout=600):
- """Main enhancement workflow
-
- Args:
- headless: If True, run claude directly without opening terminal (default: True)
- timeout: Maximum time to wait for enhancement in seconds (default: 600 = 10 minutes)
- """
- print(f"\n{'='*60}")
- print(f"LOCAL ENHANCEMENT: {self.skill_dir.name}")
- print(f"{'='*60}\n")
-
- # Validate
- if not self.skill_dir.exists():
- print(f"❌ Directory not found: {self.skill_dir}")
- return False
-
- # Read reference files
- print("📖 Reading reference documentation...")
- references = read_reference_files(
- self.skill_dir,
- max_chars=LOCAL_CONTENT_LIMIT,
- preview_limit=LOCAL_PREVIEW_LIMIT
- )
-
- if not references:
- print("❌ No reference files found to analyze")
- return False
-
- print(f" ✓ Read {len(references)} reference files")
- total_size = sum(len(c) for c in references.values())
- print(f" ✓ Total size: {total_size:,} characters\n")
-
- # Create prompt
- print("📝 Creating enhancement prompt...")
- prompt = self.create_enhancement_prompt()
-
- if not prompt:
- return False
-
- # Save prompt to temp file
- with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
- prompt_file = f.name
- f.write(prompt)
-
- print(f" ✓ Prompt saved ({len(prompt):,} characters)\n")
-
- # Headless mode: Run claude directly without opening terminal
- if headless:
- return self._run_headless(prompt_file, timeout)
-
- # Terminal mode: Launch Claude Code in new terminal
- print("🚀 Launching Claude Code in new terminal...")
- print(" This will:")
- print(" 1. Open a new terminal window")
- print(" 2. Run Claude Code with the enhancement task")
- print(" 3. Claude will read the docs and enhance SKILL.md")
- print(" 4. Terminal will auto-close when done")
- print()
-
- # Create a shell script to run in the terminal
- shell_script = f'''#!/bin/bash
-claude {prompt_file}
-echo ""
-echo "✅ Enhancement complete!"
-echo "Press any key to close..."
-read -n 1
-rm {prompt_file}
-'''
-
- # Save shell script
- with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
- script_file = f.name
- f.write(shell_script)
-
- os.chmod(script_file, 0o755)
-
- # Launch in new terminal (macOS specific)
- if sys.platform == 'darwin':
- # Detect which terminal app to use
- terminal_app, detection_method = detect_terminal_app()
-
- # Show detection info
- if detection_method == 'SKILL_SEEKER_TERMINAL':
- print(f" Using terminal: {terminal_app} (from SKILL_SEEKER_TERMINAL)")
- elif detection_method == 'TERM_PROGRAM':
- print(f" Using terminal: {terminal_app} (inherited from current terminal)")
- elif detection_method.startswith('unknown TERM_PROGRAM'):
- print(f"⚠️ {detection_method}")
- print(f" → Using Terminal.app as fallback")
- else:
- print(f" Using terminal: {terminal_app} (default)")
-
- try:
- subprocess.Popen(['open', '-a', terminal_app, script_file])
- except Exception as e:
- print(f"⚠️ Error launching {terminal_app}: {e}")
- print(f"\nManually run: {script_file}")
- return False
- else:
- print("⚠️ Auto-launch only works on macOS")
- print(f"\nManually run this command in a new terminal:")
- print(f" claude '{prompt_file}'")
- print(f"\nThen delete the prompt file:")
- print(f" rm '{prompt_file}'")
- return False
-
- print("✅ New terminal launched with Claude Code!")
- print()
- print("📊 Status:")
- print(f" - Prompt file: {prompt_file}")
- print(f" - Skill directory: {self.skill_dir.absolute()}")
- print(f" - SKILL.md will be saved to: {self.skill_md_path.absolute()}")
- print(f" - Original backed up to: {self.skill_md_path.with_suffix('.md.backup').absolute()}")
- print()
- print("⏳ Wait for Claude Code to finish in the other terminal...")
- print(" (Usually takes 30-60 seconds)")
- print()
- print("💡 When done:")
- print(f" 1. Check the enhanced SKILL.md: {self.skill_md_path}")
- print(f" 2. If you don't like it, restore: mv {self.skill_md_path.with_suffix('.md.backup')} {self.skill_md_path}")
- print(f" 3. Package: skill-seekers package {self.skill_dir}/")
-
- return True
-
- def _run_headless(self, prompt_file, timeout):
- """Run Claude enhancement in headless mode (no terminal window)
-
- Args:
- prompt_file: Path to prompt file
- timeout: Maximum seconds to wait
-
- Returns:
- bool: True if enhancement succeeded
- """
- import time
- from pathlib import Path
-
- print("✨ Running Claude Code enhancement (headless mode)...")
- print(f" Timeout: {timeout} seconds ({timeout//60} minutes)")
- print()
-
- # Record initial state
- initial_mtime = self.skill_md_path.stat().st_mtime if self.skill_md_path.exists() else 0
- initial_size = self.skill_md_path.stat().st_size if self.skill_md_path.exists() else 0
-
- # Start timer
- start_time = time.time()
-
- try:
- # Run claude command directly (this WAITS for completion)
- print(" Running: claude {prompt_file}")
- print(" ⏳ Please wait...")
- print()
-
- result = subprocess.run(
- ['claude', prompt_file],
- capture_output=True,
- text=True,
- timeout=timeout
- )
-
- elapsed = time.time() - start_time
-
- # Check if successful
- if result.returncode == 0:
- # Verify SKILL.md was actually updated
- if self.skill_md_path.exists():
- new_mtime = self.skill_md_path.stat().st_mtime
- new_size = self.skill_md_path.stat().st_size
-
- if new_mtime > initial_mtime and new_size > initial_size:
- print(f"✅ Enhancement complete! ({elapsed:.1f} seconds)")
- print(f" SKILL.md updated: {new_size:,} bytes")
- print()
-
- # Clean up prompt file
- try:
- os.unlink(prompt_file)
- except:
- pass
-
- return True
- else:
- print(f"⚠️ Claude finished but SKILL.md was not updated")
- print(f" This might indicate an error during enhancement")
- print()
- return False
- else:
- print(f"❌ SKILL.md not found after enhancement")
- return False
- else:
- print(f"❌ Claude Code returned error (exit code: {result.returncode})")
- if result.stderr:
- print(f" Error: {result.stderr[:200]}")
- return False
-
- except subprocess.TimeoutExpired:
- elapsed = time.time() - start_time
- print(f"\n⚠️ Enhancement timed out after {elapsed:.0f} seconds")
- print(f" Timeout limit: {timeout} seconds")
- print()
- print(" Possible reasons:")
- print(" - Skill is very large (many references)")
- print(" - Claude is taking longer than usual")
- print(" - Network issues")
- print()
- print(" Try:")
- print(" 1. Use terminal mode: --interactive-enhancement")
- print(" 2. Reduce reference content")
- print(" 3. Try again later")
-
- # Clean up
- try:
- os.unlink(prompt_file)
- except:
- pass
-
- return False
-
- except FileNotFoundError:
- print("❌ 'claude' command not found")
- print()
- print(" Make sure Claude Code CLI is installed:")
- print(" See: https://docs.claude.com/claude-code")
- print()
- print(" Try terminal mode instead: --interactive-enhancement")
-
- return False
-
- except Exception as e:
- print(f"❌ Unexpected error: {e}")
- return False
-
-
-def main():
- import argparse
-
- parser = argparse.ArgumentParser(
- description="Enhance a skill with Claude Code (local)",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Headless mode (default - runs in background)
- skill-seekers enhance output/react/
-
- # Interactive mode (opens terminal window)
- skill-seekers enhance output/react/ --interactive-enhancement
-
- # Custom timeout
- skill-seekers enhance output/react/ --timeout 1200
-"""
- )
-
- parser.add_argument(
- 'skill_directory',
- help='Path to skill directory (e.g., output/react/)'
- )
-
- parser.add_argument(
- '--interactive-enhancement',
- action='store_true',
- help='Open terminal window for enhancement (default: headless mode)'
- )
-
- parser.add_argument(
- '--timeout',
- type=int,
- default=600,
- help='Timeout in seconds for headless mode (default: 600 = 10 minutes)'
- )
-
- args = parser.parse_args()
-
- # Run enhancement
- enhancer = LocalSkillEnhancer(args.skill_directory)
- headless = not args.interactive_enhancement # Invert: default is headless
- success = enhancer.run(headless=headless, timeout=args.timeout)
-
- sys.exit(0 if success else 1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/estimate_pages.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/estimate_pages.py
deleted file mode 100644
index c2a23b0..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/estimate_pages.py
+++ /dev/null
@@ -1,288 +0,0 @@
-#!/usr/bin/env python3
-"""
-Page Count Estimator for Skill Seeker
-Quickly estimates how many pages a config will scrape without downloading content
-"""
-
-import sys
-import os
-import requests
-from bs4 import BeautifulSoup
-from urllib.parse import urljoin, urlparse
-import time
-import json
-
-# Add parent directory to path for imports when run as script
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.constants import (
- DEFAULT_RATE_LIMIT,
- DEFAULT_MAX_DISCOVERY,
- DISCOVERY_THRESHOLD
-)
-
-
-def estimate_pages(config, max_discovery=DEFAULT_MAX_DISCOVERY, timeout=30):
- """
- Estimate total pages that will be scraped
-
- Args:
- config: Configuration dictionary
- max_discovery: Maximum pages to discover (safety limit, use -1 for unlimited)
- timeout: Timeout for HTTP requests in seconds
-
- Returns:
- dict with estimation results
- """
- base_url = config['base_url']
- start_urls = config.get('start_urls', [base_url])
- url_patterns = config.get('url_patterns', {'include': [], 'exclude': []})
- rate_limit = config.get('rate_limit', DEFAULT_RATE_LIMIT)
-
- visited = set()
- pending = list(start_urls)
- discovered = 0
-
- include_patterns = url_patterns.get('include', [])
- exclude_patterns = url_patterns.get('exclude', [])
-
- # Handle unlimited mode
- unlimited = (max_discovery == -1 or max_discovery is None)
-
- print(f"🔍 Estimating pages for: {config['name']}")
- print(f"📍 Base URL: {base_url}")
- print(f"🎯 Start URLs: {len(start_urls)}")
- print(f"⏱️ Rate limit: {rate_limit}s")
-
- if unlimited:
- print(f"🔢 Max discovery: UNLIMITED (will discover all pages)")
- print(f"⚠️ WARNING: This may take a long time!")
- else:
- print(f"🔢 Max discovery: {max_discovery}")
-
- print()
-
- start_time = time.time()
-
- # Loop condition: stop if no more URLs, or if limit reached (when not unlimited)
- while pending and (unlimited or discovered < max_discovery):
- url = pending.pop(0)
-
- # Skip if already visited
- if url in visited:
- continue
-
- visited.add(url)
- discovered += 1
-
- # Progress indicator
- if discovered % 10 == 0:
- elapsed = time.time() - start_time
- rate = discovered / elapsed if elapsed > 0 else 0
- print(f"⏳ Discovered: {discovered} pages ({rate:.1f} pages/sec)", end='\r')
-
- try:
- # HEAD request first to check if page exists (faster)
- head_response = requests.head(url, timeout=timeout, allow_redirects=True)
-
- # Skip non-HTML content
- content_type = head_response.headers.get('Content-Type', '')
- if 'text/html' not in content_type:
- continue
-
- # Now GET the page to find links
- response = requests.get(url, timeout=timeout)
- response.raise_for_status()
-
- soup = BeautifulSoup(response.content, 'html.parser')
-
- # Find all links
- for link in soup.find_all('a', href=True):
- href = link['href']
- full_url = urljoin(url, href)
-
- # Normalize URL
- parsed = urlparse(full_url)
- full_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
-
- # Check if URL is valid
- if not is_valid_url(full_url, base_url, include_patterns, exclude_patterns):
- continue
-
- # Add to pending if not visited
- if full_url not in visited and full_url not in pending:
- pending.append(full_url)
-
- # Rate limiting
- time.sleep(rate_limit)
-
- except requests.RequestException as e:
- # Silently skip errors during estimation
- pass
- except Exception as e:
- # Silently skip other errors
- pass
-
- elapsed = time.time() - start_time
-
- # Results
- results = {
- 'discovered': discovered,
- 'pending': len(pending),
- 'estimated_total': discovered + len(pending),
- 'elapsed_seconds': round(elapsed, 2),
- 'discovery_rate': round(discovered / elapsed if elapsed > 0 else 0, 2),
- 'hit_limit': (not unlimited) and (discovered >= max_discovery),
- 'unlimited': unlimited
- }
-
- return results
-
-
-def is_valid_url(url, base_url, include_patterns, exclude_patterns):
- """Check if URL should be crawled"""
- # Must be same domain
- if not url.startswith(base_url.rstrip('/')):
- return False
-
- # Check exclude patterns first
- if exclude_patterns:
- for pattern in exclude_patterns:
- if pattern in url:
- return False
-
- # Check include patterns (if specified)
- if include_patterns:
- for pattern in include_patterns:
- if pattern in url:
- return True
- return False
-
- # If no include patterns, accept by default
- return True
-
-
-def print_results(results, config):
- """Print estimation results"""
- print()
- print("=" * 70)
- print("📊 ESTIMATION RESULTS")
- print("=" * 70)
- print()
- print(f"Config: {config['name']}")
- print(f"Base URL: {config['base_url']}")
- print()
- print(f"✅ Pages Discovered: {results['discovered']}")
- print(f"⏳ Pages Pending: {results['pending']}")
- print(f"📈 Estimated Total: {results['estimated_total']}")
- print()
- print(f"⏱️ Time Elapsed: {results['elapsed_seconds']}s")
- print(f"⚡ Discovery Rate: {results['discovery_rate']} pages/sec")
-
- if results.get('unlimited', False):
- print()
- print("✅ UNLIMITED MODE - Discovered all reachable pages")
- print(f" Total pages: {results['estimated_total']}")
- elif results['hit_limit']:
- print()
- print("⚠️ Hit discovery limit - actual total may be higher")
- print(" Increase max_discovery parameter for more accurate estimate")
-
- print()
- print("=" * 70)
- print("💡 RECOMMENDATIONS")
- print("=" * 70)
- print()
-
- estimated = results['estimated_total']
- current_max = config.get('max_pages', 100)
-
- if estimated <= current_max:
- print(f"✅ Current max_pages ({current_max}) is sufficient")
- else:
- recommended = min(estimated + 50, DISCOVERY_THRESHOLD) # Add 50 buffer, cap at threshold
- print(f"⚠️ Current max_pages ({current_max}) may be too low")
- print(f"📝 Recommended max_pages: {recommended}")
- print(f" (Estimated {estimated} + 50 buffer)")
-
- # Estimate time for full scrape
- rate_limit = config.get('rate_limit', DEFAULT_RATE_LIMIT)
- estimated_time = (estimated * rate_limit) / 60 # in minutes
-
- print()
- print(f"⏱️ Estimated full scrape time: {estimated_time:.1f} minutes")
- print(f" (Based on rate_limit: {rate_limit}s)")
-
- print()
-
-
-def load_config(config_path):
- """Load configuration from JSON file"""
- try:
- with open(config_path, 'r') as f:
- config = json.load(f)
- return config
- except FileNotFoundError:
- print(f"❌ Error: Config file not found: {config_path}")
- sys.exit(1)
- except json.JSONDecodeError as e:
- print(f"❌ Error: Invalid JSON in config file: {e}")
- sys.exit(1)
-
-
-def main():
- """Main entry point"""
- import argparse
-
- parser = argparse.ArgumentParser(
- description='Estimate page count for Skill Seeker configs',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Estimate pages for a config
- skill-seekers estimate configs/react.json
-
- # Estimate with higher discovery limit
- skill-seekers estimate configs/godot.json --max-discovery 2000
-
- # Quick estimate (stop at 100 pages)
- skill-seekers estimate configs/vue.json --max-discovery 100
- """
- )
-
- parser.add_argument('config', help='Path to config JSON file')
- parser.add_argument('--max-discovery', '-m', type=int, default=DEFAULT_MAX_DISCOVERY,
- help=f'Maximum pages to discover (default: {DEFAULT_MAX_DISCOVERY}, use -1 for unlimited)')
- parser.add_argument('--unlimited', '-u', action='store_true',
- help='Remove discovery limit - discover all pages (same as --max-discovery -1)')
- parser.add_argument('--timeout', '-t', type=int, default=30,
- help='HTTP request timeout in seconds (default: 30)')
-
- args = parser.parse_args()
-
- # Handle unlimited flag
- max_discovery = -1 if args.unlimited else args.max_discovery
-
- # Load config
- config = load_config(args.config)
-
- # Run estimation
- try:
- results = estimate_pages(config, max_discovery, args.timeout)
- print_results(results, config)
-
- # Return exit code based on results
- if results['hit_limit']:
- return 2 # Warning: hit limit
- return 0 # Success
-
- except KeyboardInterrupt:
- print("\n\n⚠️ Estimation interrupted by user")
- return 1
- except Exception as e:
- print(f"\n\n❌ Error during estimation: {e}")
- return 1
-
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/generate_router.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/generate_router.py
deleted file mode 100644
index 0d4ef84..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/generate_router.py
+++ /dev/null
@@ -1,274 +0,0 @@
-#!/usr/bin/env python3
-"""
-Router Skill Generator
-
-Creates a router/hub skill that intelligently directs queries to specialized sub-skills.
-This is used for large documentation sites split into multiple focused skills.
-"""
-
-import json
-import sys
-import argparse
-from pathlib import Path
-from typing import Dict, List, Any, Tuple
-
-
-class RouterGenerator:
- """Generates router skills that direct to specialized sub-skills"""
-
- def __init__(self, config_paths: List[str], router_name: str = None):
- self.config_paths = [Path(p) for p in config_paths]
- self.configs = [self.load_config(p) for p in self.config_paths]
- self.router_name = router_name or self.infer_router_name()
- self.base_config = self.configs[0] # Use first as template
-
- def load_config(self, path: Path) -> Dict[str, Any]:
- """Load a config file"""
- try:
- with open(path, 'r') as f:
- return json.load(f)
- except Exception as e:
- print(f"❌ Error loading {path}: {e}")
- sys.exit(1)
-
- def infer_router_name(self) -> str:
- """Infer router name from sub-skill names"""
- # Find common prefix
- names = [cfg['name'] for cfg in self.configs]
- if not names:
- return "router"
-
- # Get common prefix before first dash
- first_name = names[0]
- if '-' in first_name:
- return first_name.split('-')[0]
- return first_name
-
- def extract_routing_keywords(self) -> Dict[str, List[str]]:
- """Extract keywords for routing to each skill"""
- routing = {}
-
- for config in self.configs:
- name = config['name']
- keywords = []
-
- # Extract from categories
- if 'categories' in config:
- keywords.extend(config['categories'].keys())
-
- # Extract from name (part after dash)
- if '-' in name:
- skill_topic = name.split('-', 1)[1]
- keywords.append(skill_topic)
-
- routing[name] = keywords
-
- return routing
-
- def generate_skill_md(self) -> str:
- """Generate router SKILL.md content"""
- routing_keywords = self.extract_routing_keywords()
-
- skill_md = f"""# {self.router_name.replace('-', ' ').title()} Documentation (Router)
-
-## When to Use This Skill
-
-{self.base_config.get('description', f'Use for {self.router_name} development and programming.')}
-
-This is a router skill that directs your questions to specialized sub-skills for efficient, focused assistance.
-
-## How It Works
-
-This skill analyzes your question and activates the appropriate specialized skill(s):
-
-"""
-
- # List sub-skills
- for config in self.configs:
- name = config['name']
- desc = config.get('description', '')
- # Remove router name prefix from description if present
- if desc.startswith(f"{self.router_name.title()} -"):
- desc = desc.split(' - ', 1)[1]
-
- skill_md += f"### {name}\n{desc}\n\n"
-
- # Routing logic
- skill_md += """## Routing Logic
-
-The router analyzes your question for topic keywords and activates relevant skills:
-
-**Keywords → Skills:**
-"""
-
- for skill_name, keywords in routing_keywords.items():
- keyword_str = ", ".join(keywords)
- skill_md += f"- {keyword_str} → **{skill_name}**\n"
-
- # Quick reference
- skill_md += f"""
-
-## Quick Reference
-
-For quick answers, this router provides basic overview information. For detailed documentation, the specialized skills contain comprehensive references.
-
-### Getting Started
-
-1. Ask your question naturally - mention the topic area
-2. The router will activate the appropriate skill(s)
-3. You'll receive focused, detailed answers from specialized documentation
-
-### Examples
-
-**Question:** "How do I create a 2D sprite?"
-**Activates:** {self.router_name}-2d skill
-
-**Question:** "GDScript function syntax"
-**Activates:** {self.router_name}-scripting skill
-
-**Question:** "Physics collision handling in 3D"
-**Activates:** {self.router_name}-3d + {self.router_name}-physics skills
-
-### All Available Skills
-
-"""
-
- # List all skills
- for config in self.configs:
- skill_md += f"- **{config['name']}**\n"
-
- skill_md += f"""
-
-## Need Help?
-
-Simply ask your question and mention the topic. The router will find the right specialized skill for you!
-
----
-
-*This is a router skill. For complete documentation, see the specialized skills listed above.*
-"""
-
- return skill_md
-
- def create_router_config(self) -> Dict[str, Any]:
- """Create router configuration"""
- routing_keywords = self.extract_routing_keywords()
-
- router_config = {
- "name": self.router_name,
- "description": self.base_config.get('description', f'{self.router_name.title()} documentation router'),
- "base_url": self.base_config['base_url'],
- "selectors": self.base_config.get('selectors', {}),
- "url_patterns": self.base_config.get('url_patterns', {}),
- "rate_limit": self.base_config.get('rate_limit', 0.5),
- "max_pages": 500, # Router only scrapes overview pages
- "_router": True,
- "_sub_skills": [cfg['name'] for cfg in self.configs],
- "_routing_keywords": routing_keywords
- }
-
- return router_config
-
- def generate(self, output_dir: Path = None) -> Tuple[Path, Path]:
- """Generate router skill and config"""
- if output_dir is None:
- output_dir = self.config_paths[0].parent
-
- output_dir = Path(output_dir)
-
- # Generate SKILL.md
- skill_md = self.generate_skill_md()
- skill_path = output_dir.parent / f"output/{self.router_name}/SKILL.md"
- skill_path.parent.mkdir(parents=True, exist_ok=True)
-
- with open(skill_path, 'w') as f:
- f.write(skill_md)
-
- # Generate config
- router_config = self.create_router_config()
- config_path = output_dir / f"{self.router_name}.json"
-
- with open(config_path, 'w') as f:
- json.dump(router_config, f, indent=2)
-
- return config_path, skill_path
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Generate router/hub skill for split documentation",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Generate router from multiple configs
- python3 generate_router.py configs/godot-2d.json configs/godot-3d.json configs/godot-scripting.json
-
- # Use glob pattern
- python3 generate_router.py configs/godot-*.json
-
- # Custom router name
- python3 generate_router.py configs/godot-*.json --name godot-hub
-
- # Custom output directory
- python3 generate_router.py configs/godot-*.json --output-dir configs/routers/
- """
- )
-
- parser.add_argument(
- 'configs',
- nargs='+',
- help='Sub-skill config files'
- )
-
- parser.add_argument(
- '--name',
- help='Router skill name (default: inferred from sub-skills)'
- )
-
- parser.add_argument(
- '--output-dir',
- help='Output directory (default: same as input configs)'
- )
-
- args = parser.parse_args()
-
- # Filter out router configs (avoid recursion)
- config_files = []
- for path_str in args.configs:
- path = Path(path_str)
- if path.exists() and not path.stem.endswith('-router'):
- config_files.append(path_str)
-
- if not config_files:
- print("❌ Error: No valid config files provided")
- sys.exit(1)
-
- print(f"\n{'='*60}")
- print("ROUTER SKILL GENERATOR")
- print(f"{'='*60}")
- print(f"Sub-skills: {len(config_files)}")
- for cfg in config_files:
- print(f" - {Path(cfg).stem}")
- print("")
-
- # Generate router
- generator = RouterGenerator(config_files, args.name)
- config_path, skill_path = generator.generate(args.output_dir)
-
- print(f"✅ Router config created: {config_path}")
- print(f"✅ Router SKILL.md created: {skill_path}")
- print("")
- print(f"{'='*60}")
- print("NEXT STEPS")
- print(f"{'='*60}")
- print(f"1. Review router SKILL.md: {skill_path}")
- print(f"2. Optionally scrape router (for overview pages):")
- print(f" skill-seekers scrape --config {config_path}")
- print("3. Package router skill:")
- print(f" skill-seekers package output/{generator.router_name}/")
- print("4. Upload router + all sub-skills to Claude")
- print("")
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/github_scraper.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/github_scraper.py
deleted file mode 100644
index 861f6c6..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/github_scraper.py
+++ /dev/null
@@ -1,900 +0,0 @@
-#!/usr/bin/env python3
-"""
-GitHub Repository to Claude Skill Converter (Tasks C1.1-C1.12)
-
-Converts GitHub repositories into Claude AI skills by extracting:
-- README and documentation
-- Code structure and signatures
-- GitHub Issues, Changelog, and Releases
-- Usage examples from tests
-
-Usage:
- skill-seekers github --repo facebook/react
- skill-seekers github --config configs/react_github.json
- skill-seekers github --repo owner/repo --token $GITHUB_TOKEN
-"""
-
-import os
-import sys
-import json
-import re
-import argparse
-import logging
-from pathlib import Path
-from typing import Dict, List, Optional, Any
-from datetime import datetime
-
-try:
- from github import Github, GithubException, Repository
- from github.GithubException import RateLimitExceededException
-except ImportError:
- print("Error: PyGithub not installed. Run: pip install PyGithub")
- sys.exit(1)
-
-# Configure logging FIRST (before using logger)
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-# Import code analyzer for deep code analysis
-try:
- from .code_analyzer import CodeAnalyzer
- CODE_ANALYZER_AVAILABLE = True
-except ImportError:
- CODE_ANALYZER_AVAILABLE = False
- logger.warning("Code analyzer not available - deep analysis disabled")
-
-# Directories to exclude from local repository analysis
-EXCLUDED_DIRS = {
- 'venv', 'env', '.venv', '.env', # Virtual environments
- 'node_modules', '__pycache__', '.pytest_cache', # Dependencies and caches
- '.git', '.svn', '.hg', # Version control
- 'build', 'dist', '*.egg-info', # Build artifacts
- 'htmlcov', '.coverage', # Coverage reports
- '.tox', '.nox', # Testing environments
- '.mypy_cache', '.ruff_cache', # Linter caches
-}
-
-
-class GitHubScraper:
- """
- GitHub Repository Scraper (C1.1-C1.9)
-
- Extracts repository information for skill generation:
- - Repository structure
- - README files
- - Code comments and docstrings
- - Programming language detection
- - Function/class signatures
- - Test examples
- - GitHub Issues
- - CHANGELOG
- - Releases
- """
-
- def __init__(self, config: Dict[str, Any], local_repo_path: Optional[str] = None):
- """Initialize GitHub scraper with configuration."""
- self.config = config
- self.repo_name = config['repo']
- self.name = config.get('name', self.repo_name.split('/')[-1])
- self.description = config.get('description', f'Skill for {self.repo_name}')
-
- # Local repository path (optional - enables unlimited analysis)
- self.local_repo_path = local_repo_path or config.get('local_repo_path')
- if self.local_repo_path:
- self.local_repo_path = os.path.expanduser(self.local_repo_path)
- logger.info(f"Local repository mode enabled: {self.local_repo_path}")
-
- # Configure directory exclusions (smart defaults + optional customization)
- self.excluded_dirs = set(EXCLUDED_DIRS) # Start with smart defaults
-
- # Option 1: Replace mode - Use only specified exclusions
- if 'exclude_dirs' in config:
- self.excluded_dirs = set(config['exclude_dirs'])
- logger.warning(
- f"Using custom directory exclusions ({len(self.excluded_dirs)} dirs) - "
- "defaults overridden"
- )
- logger.debug(f"Custom exclusions: {sorted(self.excluded_dirs)}")
-
- # Option 2: Extend mode - Add to default exclusions
- elif 'exclude_dirs_additional' in config:
- additional = set(config['exclude_dirs_additional'])
- self.excluded_dirs = self.excluded_dirs.union(additional)
- logger.info(
- f"Added {len(additional)} custom directory exclusions "
- f"(total: {len(self.excluded_dirs)})"
- )
- logger.debug(f"Additional exclusions: {sorted(additional)}")
-
- # GitHub client setup (C1.1)
- token = self._get_token()
- self.github = Github(token) if token else Github()
- self.repo: Optional[Repository.Repository] = None
-
- # Options
- self.include_issues = config.get('include_issues', True)
- self.max_issues = config.get('max_issues', 100)
- self.include_changelog = config.get('include_changelog', True)
- self.include_releases = config.get('include_releases', True)
- self.include_code = config.get('include_code', False)
- self.code_analysis_depth = config.get('code_analysis_depth', 'surface') # 'surface', 'deep', 'full'
- self.file_patterns = config.get('file_patterns', [])
-
- # Initialize code analyzer if deep analysis requested
- self.code_analyzer = None
- if self.code_analysis_depth != 'surface' and CODE_ANALYZER_AVAILABLE:
- self.code_analyzer = CodeAnalyzer(depth=self.code_analysis_depth)
- logger.info(f"Code analysis depth: {self.code_analysis_depth}")
-
- # Output paths
- self.skill_dir = f"output/{self.name}"
- self.data_file = f"output/{self.name}_github_data.json"
-
- # Extracted data storage
- self.extracted_data = {
- 'repo_info': {},
- 'readme': '',
- 'file_tree': [],
- 'languages': {},
- 'signatures': [],
- 'test_examples': [],
- 'issues': [],
- 'changelog': '',
- 'releases': []
- }
-
- def _get_token(self) -> Optional[str]:
- """
- Get GitHub token from env var or config (both options supported).
- Priority: GITHUB_TOKEN env var > config file > None
- """
- # Try environment variable first (recommended)
- token = os.getenv('GITHUB_TOKEN')
- if token:
- logger.info("Using GitHub token from GITHUB_TOKEN environment variable")
- return token
-
- # Fall back to config file
- token = self.config.get('github_token')
- if token:
- logger.warning("Using GitHub token from config file (less secure)")
- return token
-
- logger.warning("No GitHub token provided - using unauthenticated access (lower rate limits)")
- return None
-
- def scrape(self) -> Dict[str, Any]:
- """
- Main scraping entry point.
- Executes all C1 tasks in sequence.
- """
- try:
- logger.info(f"Starting GitHub scrape for: {self.repo_name}")
-
- # C1.1: Fetch repository
- self._fetch_repository()
-
- # C1.2: Extract README
- self._extract_readme()
-
- # C1.3-C1.6: Extract code structure
- self._extract_code_structure()
-
- # C1.7: Extract Issues
- if self.include_issues:
- self._extract_issues()
-
- # C1.8: Extract CHANGELOG
- if self.include_changelog:
- self._extract_changelog()
-
- # C1.9: Extract Releases
- if self.include_releases:
- self._extract_releases()
-
- # Save extracted data
- self._save_data()
-
- logger.info(f"✅ Scraping complete! Data saved to: {self.data_file}")
- return self.extracted_data
-
- except RateLimitExceededException:
- logger.error("GitHub API rate limit exceeded. Please wait or use authentication token.")
- raise
- except GithubException as e:
- logger.error(f"GitHub API error: {e}")
- raise
- except Exception as e:
- logger.error(f"Unexpected error during scraping: {e}")
- raise
-
- def _fetch_repository(self):
- """C1.1: Fetch repository structure using GitHub API."""
- logger.info(f"Fetching repository: {self.repo_name}")
-
- try:
- self.repo = self.github.get_repo(self.repo_name)
-
- # Extract basic repo info
- self.extracted_data['repo_info'] = {
- 'name': self.repo.name,
- 'full_name': self.repo.full_name,
- 'description': self.repo.description,
- 'url': self.repo.html_url,
- 'homepage': self.repo.homepage,
- 'stars': self.repo.stargazers_count,
- 'forks': self.repo.forks_count,
- 'open_issues': self.repo.open_issues_count,
- 'default_branch': self.repo.default_branch,
- 'created_at': self.repo.created_at.isoformat() if self.repo.created_at else None,
- 'updated_at': self.repo.updated_at.isoformat() if self.repo.updated_at else None,
- 'language': self.repo.language,
- 'license': self.repo.license.name if self.repo.license else None,
- 'topics': self.repo.get_topics()
- }
-
- logger.info(f"Repository fetched: {self.repo.full_name} ({self.repo.stargazers_count} stars)")
-
- except GithubException as e:
- if e.status == 404:
- raise ValueError(f"Repository not found: {self.repo_name}")
- raise
-
- def _extract_readme(self):
- """C1.2: Extract README.md files."""
- logger.info("Extracting README...")
-
- # Try common README locations
- readme_files = ['README.md', 'README.rst', 'README.txt', 'README',
- 'docs/README.md', '.github/README.md']
-
- for readme_path in readme_files:
- try:
- content = self.repo.get_contents(readme_path)
- if content:
- self.extracted_data['readme'] = content.decoded_content.decode('utf-8')
- logger.info(f"README found: {readme_path}")
- return
- except GithubException:
- continue
-
- logger.warning("No README found in repository")
-
- def _extract_code_structure(self):
- """
- C1.3-C1.6: Extract code structure, languages, signatures, and test examples.
- Surface layer only - no full implementation code.
- """
- logger.info("Extracting code structure...")
-
- # C1.4: Get language breakdown
- self._extract_languages()
-
- # Get file tree
- self._extract_file_tree()
-
- # Extract signatures and test examples
- if self.include_code:
- self._extract_signatures_and_tests()
-
- def _extract_languages(self):
- """C1.4: Detect programming languages in repository."""
- logger.info("Detecting programming languages...")
-
- try:
- languages = self.repo.get_languages()
- total_bytes = sum(languages.values())
-
- self.extracted_data['languages'] = {
- lang: {
- 'bytes': bytes_count,
- 'percentage': round((bytes_count / total_bytes) * 100, 2) if total_bytes > 0 else 0
- }
- for lang, bytes_count in languages.items()
- }
-
- logger.info(f"Languages detected: {', '.join(languages.keys())}")
-
- except GithubException as e:
- logger.warning(f"Could not fetch languages: {e}")
-
- def should_exclude_dir(self, dir_name: str) -> bool:
- """Check if directory should be excluded from analysis."""
- return dir_name in self.excluded_dirs or dir_name.startswith('.')
-
- def _extract_file_tree(self):
- """Extract repository file tree structure (dual-mode: GitHub API or local filesystem)."""
- logger.info("Building file tree...")
-
- if self.local_repo_path:
- # Local filesystem mode - unlimited files
- self._extract_file_tree_local()
- else:
- # GitHub API mode - limited by API rate limits
- self._extract_file_tree_github()
-
- def _extract_file_tree_local(self):
- """Extract file tree from local filesystem (unlimited files)."""
- if not os.path.exists(self.local_repo_path):
- logger.error(f"Local repository path not found: {self.local_repo_path}")
- return
-
- file_tree = []
- for root, dirs, files in os.walk(self.local_repo_path):
- # Exclude directories in-place to prevent os.walk from descending into them
- dirs[:] = [d for d in dirs if not self.should_exclude_dir(d)]
-
- # Calculate relative path from repo root
- rel_root = os.path.relpath(root, self.local_repo_path)
- if rel_root == '.':
- rel_root = ''
-
- # Add directories
- for dir_name in dirs:
- dir_path = os.path.join(rel_root, dir_name) if rel_root else dir_name
- file_tree.append({
- 'path': dir_path,
- 'type': 'dir',
- 'size': None
- })
-
- # Add files
- for file_name in files:
- file_path = os.path.join(rel_root, file_name) if rel_root else file_name
- full_path = os.path.join(root, file_name)
- try:
- file_size = os.path.getsize(full_path)
- except OSError:
- file_size = None
-
- file_tree.append({
- 'path': file_path,
- 'type': 'file',
- 'size': file_size
- })
-
- self.extracted_data['file_tree'] = file_tree
- logger.info(f"File tree built (local mode): {len(file_tree)} items")
-
- def _extract_file_tree_github(self):
- """Extract file tree from GitHub API (rate-limited)."""
- try:
- contents = self.repo.get_contents("")
- file_tree = []
-
- while contents:
- file_content = contents.pop(0)
-
- file_info = {
- 'path': file_content.path,
- 'type': file_content.type,
- 'size': file_content.size if file_content.type == 'file' else None
- }
- file_tree.append(file_info)
-
- if file_content.type == "dir":
- contents.extend(self.repo.get_contents(file_content.path))
-
- self.extracted_data['file_tree'] = file_tree
- logger.info(f"File tree built (GitHub API mode): {len(file_tree)} items")
-
- except GithubException as e:
- logger.warning(f"Could not build file tree: {e}")
-
- def _extract_signatures_and_tests(self):
- """
- C1.3, C1.5, C1.6: Extract signatures, docstrings, and test examples.
-
- Extraction depth depends on code_analysis_depth setting:
- - surface: File tree only (minimal)
- - deep: Parse files for signatures, parameters, types
- - full: Complete AST analysis (future enhancement)
- """
- if self.code_analysis_depth == 'surface':
- logger.info("Code extraction: Surface level (file tree only)")
- return
-
- if not self.code_analyzer:
- logger.warning("Code analyzer not available - skipping deep analysis")
- return
-
- logger.info(f"Extracting code signatures ({self.code_analysis_depth} analysis)...")
-
- # Get primary language for the repository
- languages = self.extracted_data.get('languages', {})
- if not languages:
- logger.warning("No languages detected - skipping code analysis")
- return
-
- # Determine primary language
- primary_language = max(languages.items(), key=lambda x: x[1]['bytes'])[0]
- logger.info(f"Primary language: {primary_language}")
-
- # Determine file extensions to analyze
- extension_map = {
- 'Python': ['.py'],
- 'JavaScript': ['.js', '.jsx'],
- 'TypeScript': ['.ts', '.tsx'],
- 'C': ['.c', '.h'],
- 'C++': ['.cpp', '.hpp', '.cc', '.hh', '.cxx']
- }
-
- extensions = extension_map.get(primary_language, [])
- if not extensions:
- logger.warning(f"No file extensions mapped for {primary_language}")
- return
-
- # Analyze files matching patterns and extensions
- analyzed_files = []
- file_tree = self.extracted_data.get('file_tree', [])
-
- for file_info in file_tree:
- file_path = file_info['path']
-
- # Check if file matches extension
- if not any(file_path.endswith(ext) for ext in extensions):
- continue
-
- # Check if file matches patterns (if specified)
- if self.file_patterns:
- import fnmatch
- if not any(fnmatch.fnmatch(file_path, pattern) for pattern in self.file_patterns):
- continue
-
- # Analyze this file
- try:
- # Read file content based on mode
- if self.local_repo_path:
- # Local mode - read from filesystem
- full_path = os.path.join(self.local_repo_path, file_path)
- with open(full_path, 'r', encoding='utf-8') as f:
- content = f.read()
- else:
- # GitHub API mode - fetch from API
- file_content = self.repo.get_contents(file_path)
- content = file_content.decoded_content.decode('utf-8')
-
- analysis_result = self.code_analyzer.analyze_file(
- file_path,
- content,
- primary_language
- )
-
- if analysis_result and (analysis_result.get('classes') or analysis_result.get('functions')):
- analyzed_files.append({
- 'file': file_path,
- 'language': primary_language,
- **analysis_result
- })
-
- logger.debug(f"Analyzed {file_path}: "
- f"{len(analysis_result.get('classes', []))} classes, "
- f"{len(analysis_result.get('functions', []))} functions")
-
- except Exception as e:
- logger.debug(f"Could not analyze {file_path}: {e}")
- continue
-
- # Limit number of files analyzed to avoid rate limits (GitHub API mode only)
- if not self.local_repo_path and len(analyzed_files) >= 50:
- logger.info(f"Reached analysis limit (50 files, GitHub API mode)")
- break
-
- self.extracted_data['code_analysis'] = {
- 'depth': self.code_analysis_depth,
- 'language': primary_language,
- 'files_analyzed': len(analyzed_files),
- 'files': analyzed_files
- }
-
- # Calculate totals
- total_classes = sum(len(f.get('classes', [])) for f in analyzed_files)
- total_functions = sum(len(f.get('functions', [])) for f in analyzed_files)
-
- logger.info(f"Code analysis complete: {len(analyzed_files)} files, "
- f"{total_classes} classes, {total_functions} functions")
-
- def _extract_issues(self):
- """C1.7: Extract GitHub Issues (open/closed, labels, milestones)."""
- logger.info(f"Extracting GitHub Issues (max {self.max_issues})...")
-
- try:
- # Fetch recent issues (open + closed)
- issues = self.repo.get_issues(state='all', sort='updated', direction='desc')
-
- issue_list = []
- for issue in issues[:self.max_issues]:
- # Skip pull requests (they appear in issues)
- if issue.pull_request:
- continue
-
- issue_data = {
- 'number': issue.number,
- 'title': issue.title,
- 'state': issue.state,
- 'labels': [label.name for label in issue.labels],
- 'milestone': issue.milestone.title if issue.milestone else None,
- 'created_at': issue.created_at.isoformat() if issue.created_at else None,
- 'updated_at': issue.updated_at.isoformat() if issue.updated_at else None,
- 'closed_at': issue.closed_at.isoformat() if issue.closed_at else None,
- 'url': issue.html_url,
- 'body': issue.body[:500] if issue.body else None # First 500 chars
- }
- issue_list.append(issue_data)
-
- self.extracted_data['issues'] = issue_list
- logger.info(f"Extracted {len(issue_list)} issues")
-
- except GithubException as e:
- logger.warning(f"Could not fetch issues: {e}")
-
- def _extract_changelog(self):
- """C1.8: Extract CHANGELOG.md and release notes."""
- logger.info("Extracting CHANGELOG...")
-
- # Try common changelog locations
- changelog_files = ['CHANGELOG.md', 'CHANGES.md', 'HISTORY.md',
- 'CHANGELOG.rst', 'CHANGELOG.txt', 'CHANGELOG',
- 'docs/CHANGELOG.md', '.github/CHANGELOG.md']
-
- for changelog_path in changelog_files:
- try:
- content = self.repo.get_contents(changelog_path)
- if content:
- self.extracted_data['changelog'] = content.decoded_content.decode('utf-8')
- logger.info(f"CHANGELOG found: {changelog_path}")
- return
- except GithubException:
- continue
-
- logger.warning("No CHANGELOG found in repository")
-
- def _extract_releases(self):
- """C1.9: Extract GitHub Releases with version history."""
- logger.info("Extracting GitHub Releases...")
-
- try:
- releases = self.repo.get_releases()
-
- release_list = []
- for release in releases:
- release_data = {
- 'tag_name': release.tag_name,
- 'name': release.title,
- 'body': release.body,
- 'draft': release.draft,
- 'prerelease': release.prerelease,
- 'created_at': release.created_at.isoformat() if release.created_at else None,
- 'published_at': release.published_at.isoformat() if release.published_at else None,
- 'url': release.html_url,
- 'tarball_url': release.tarball_url,
- 'zipball_url': release.zipball_url
- }
- release_list.append(release_data)
-
- self.extracted_data['releases'] = release_list
- logger.info(f"Extracted {len(release_list)} releases")
-
- except GithubException as e:
- logger.warning(f"Could not fetch releases: {e}")
-
- def _save_data(self):
- """Save extracted data to JSON file."""
- os.makedirs('output', exist_ok=True)
-
- with open(self.data_file, 'w', encoding='utf-8') as f:
- json.dump(self.extracted_data, f, indent=2, ensure_ascii=False)
-
- logger.info(f"Data saved to: {self.data_file}")
-
-
-class GitHubToSkillConverter:
- """
- Convert extracted GitHub data to Claude skill format (C1.10).
- """
-
- def __init__(self, config: Dict[str, Any]):
- """Initialize converter with configuration."""
- self.config = config
- self.name = config.get('name', config['repo'].split('/')[-1])
- self.description = config.get('description', f'Skill for {config["repo"]}')
-
- # Paths
- self.data_file = f"output/{self.name}_github_data.json"
- self.skill_dir = f"output/{self.name}"
-
- # Load extracted data
- self.data = self._load_data()
-
- def _load_data(self) -> Dict[str, Any]:
- """Load extracted GitHub data from JSON."""
- if not os.path.exists(self.data_file):
- raise FileNotFoundError(f"Data file not found: {self.data_file}")
-
- with open(self.data_file, 'r', encoding='utf-8') as f:
- return json.load(f)
-
- def build_skill(self):
- """Build complete skill structure."""
- logger.info(f"Building skill for: {self.name}")
-
- # Create directories
- os.makedirs(self.skill_dir, exist_ok=True)
- os.makedirs(f"{self.skill_dir}/references", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/scripts", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/assets", exist_ok=True)
-
- # Generate SKILL.md
- self._generate_skill_md()
-
- # Generate reference files
- self._generate_references()
-
- logger.info(f"✅ Skill built successfully: {self.skill_dir}/")
-
- def _generate_skill_md(self):
- """Generate main SKILL.md file."""
- repo_info = self.data.get('repo_info', {})
-
- # Generate skill name (lowercase, hyphens only, max 64 chars)
- skill_name = self.name.lower().replace('_', '-').replace(' ', '-')[:64]
-
- # Truncate description to 1024 chars if needed
- desc = self.description[:1024] if len(self.description) > 1024 else self.description
-
- skill_content = f"""---
-name: {skill_name}
-description: {desc}
----
-
-# {repo_info.get('name', self.name)}
-
-{self.description}
-
-## Description
-
-{repo_info.get('description', 'GitHub repository skill')}
-
-**Repository:** [{repo_info.get('full_name', 'N/A')}]({repo_info.get('url', '#')})
-**Language:** {repo_info.get('language', 'N/A')}
-**Stars:** {repo_info.get('stars', 0):,}
-**License:** {repo_info.get('license', 'N/A')}
-
-## When to Use This Skill
-
-Use this skill when you need to:
-- Understand how to use {self.name}
-- Look up API documentation
-- Find usage examples
-- Check for known issues or recent changes
-- Review release history
-
-## Quick Reference
-
-### Repository Info
-- **Homepage:** {repo_info.get('homepage', 'N/A')}
-- **Topics:** {', '.join(repo_info.get('topics', []))}
-- **Open Issues:** {repo_info.get('open_issues', 0)}
-- **Last Updated:** {repo_info.get('updated_at', 'N/A')[:10]}
-
-### Languages
-{self._format_languages()}
-
-### Recent Releases
-{self._format_recent_releases()}
-
-## Available References
-
-- `references/README.md` - Complete README documentation
-- `references/CHANGELOG.md` - Version history and changes
-- `references/issues.md` - Recent GitHub issues
-- `references/releases.md` - Release notes
-- `references/file_structure.md` - Repository structure
-
-## Usage
-
-See README.md for complete usage instructions and examples.
-
----
-
-**Generated by Skill Seeker** | GitHub Repository Scraper
-"""
-
- skill_path = f"{self.skill_dir}/SKILL.md"
- with open(skill_path, 'w', encoding='utf-8') as f:
- f.write(skill_content)
-
- logger.info(f"Generated: {skill_path}")
-
- def _format_languages(self) -> str:
- """Format language breakdown."""
- languages = self.data.get('languages', {})
- if not languages:
- return "No language data available"
-
- lines = []
- for lang, info in sorted(languages.items(), key=lambda x: x[1]['bytes'], reverse=True):
- lines.append(f"- **{lang}:** {info['percentage']:.1f}%")
-
- return '\n'.join(lines)
-
- def _format_recent_releases(self) -> str:
- """Format recent releases (top 3)."""
- releases = self.data.get('releases', [])
- if not releases:
- return "No releases available"
-
- lines = []
- for release in releases[:3]:
- lines.append(f"- **{release['tag_name']}** ({release['published_at'][:10]}): {release['name']}")
-
- return '\n'.join(lines)
-
- def _generate_references(self):
- """Generate all reference files."""
- # README
- if self.data.get('readme'):
- readme_path = f"{self.skill_dir}/references/README.md"
- with open(readme_path, 'w', encoding='utf-8') as f:
- f.write(self.data['readme'])
- logger.info(f"Generated: {readme_path}")
-
- # CHANGELOG
- if self.data.get('changelog'):
- changelog_path = f"{self.skill_dir}/references/CHANGELOG.md"
- with open(changelog_path, 'w', encoding='utf-8') as f:
- f.write(self.data['changelog'])
- logger.info(f"Generated: {changelog_path}")
-
- # Issues
- if self.data.get('issues'):
- self._generate_issues_reference()
-
- # Releases
- if self.data.get('releases'):
- self._generate_releases_reference()
-
- # File structure
- if self.data.get('file_tree'):
- self._generate_file_structure_reference()
-
- def _generate_issues_reference(self):
- """Generate issues.md reference file."""
- issues = self.data['issues']
-
- content = f"# GitHub Issues\n\nRecent issues from the repository ({len(issues)} total).\n\n"
-
- # Group by state
- open_issues = [i for i in issues if i['state'] == 'open']
- closed_issues = [i for i in issues if i['state'] == 'closed']
-
- content += f"## Open Issues ({len(open_issues)})\n\n"
- for issue in open_issues[:20]:
- labels = ', '.join(issue['labels']) if issue['labels'] else 'No labels'
- content += f"### #{issue['number']}: {issue['title']}\n"
- content += f"**Labels:** {labels} | **Created:** {issue['created_at'][:10]}\n"
- content += f"[View on GitHub]({issue['url']})\n\n"
-
- content += f"\n## Recently Closed Issues ({len(closed_issues)})\n\n"
- for issue in closed_issues[:10]:
- labels = ', '.join(issue['labels']) if issue['labels'] else 'No labels'
- content += f"### #{issue['number']}: {issue['title']}\n"
- content += f"**Labels:** {labels} | **Closed:** {issue['closed_at'][:10]}\n"
- content += f"[View on GitHub]({issue['url']})\n\n"
-
- issues_path = f"{self.skill_dir}/references/issues.md"
- with open(issues_path, 'w', encoding='utf-8') as f:
- f.write(content)
- logger.info(f"Generated: {issues_path}")
-
- def _generate_releases_reference(self):
- """Generate releases.md reference file."""
- releases = self.data['releases']
-
- content = f"# Releases\n\nVersion history for this repository ({len(releases)} releases).\n\n"
-
- for release in releases:
- content += f"## {release['tag_name']}: {release['name']}\n"
- content += f"**Published:** {release['published_at'][:10]}\n"
- if release['prerelease']:
- content += f"**Pre-release**\n"
- content += f"\n{release['body']}\n\n"
- content += f"[View on GitHub]({release['url']})\n\n---\n\n"
-
- releases_path = f"{self.skill_dir}/references/releases.md"
- with open(releases_path, 'w', encoding='utf-8') as f:
- f.write(content)
- logger.info(f"Generated: {releases_path}")
-
- def _generate_file_structure_reference(self):
- """Generate file_structure.md reference file."""
- file_tree = self.data['file_tree']
-
- content = f"# Repository File Structure\n\n"
- content += f"Total items: {len(file_tree)}\n\n"
- content += "```\n"
-
- # Build tree structure
- for item in file_tree:
- indent = " " * item['path'].count('/')
- icon = "📁" if item['type'] == 'dir' else "📄"
- content += f"{indent}{icon} {os.path.basename(item['path'])}\n"
-
- content += "```\n"
-
- structure_path = f"{self.skill_dir}/references/file_structure.md"
- with open(structure_path, 'w', encoding='utf-8') as f:
- f.write(content)
- logger.info(f"Generated: {structure_path}")
-
-
-def main():
- """C1.10: CLI tool entry point."""
- parser = argparse.ArgumentParser(
- description='GitHub Repository to Claude Skill Converter',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- skill-seekers github --repo facebook/react
- skill-seekers github --config configs/react_github.json
- skill-seekers github --repo owner/repo --token $GITHUB_TOKEN
- """
- )
-
- parser.add_argument('--repo', help='GitHub repository (owner/repo)')
- parser.add_argument('--config', help='Path to config JSON file')
- parser.add_argument('--token', help='GitHub personal access token')
- parser.add_argument('--name', help='Skill name (default: repo name)')
- parser.add_argument('--description', help='Skill description')
- parser.add_argument('--no-issues', action='store_true', help='Skip GitHub issues')
- parser.add_argument('--no-changelog', action='store_true', help='Skip CHANGELOG')
- parser.add_argument('--no-releases', action='store_true', help='Skip releases')
- parser.add_argument('--max-issues', type=int, default=100, help='Max issues to fetch')
- parser.add_argument('--scrape-only', action='store_true', help='Only scrape, don\'t build skill')
-
- args = parser.parse_args()
-
- # Build config from args or file
- if args.config:
- with open(args.config, 'r') as f:
- config = json.load(f)
- elif args.repo:
- config = {
- 'repo': args.repo,
- 'name': args.name or args.repo.split('/')[-1],
- 'description': args.description or f'GitHub repository skill for {args.repo}',
- 'github_token': args.token,
- 'include_issues': not args.no_issues,
- 'include_changelog': not args.no_changelog,
- 'include_releases': not args.no_releases,
- 'max_issues': args.max_issues
- }
- else:
- parser.error('Either --repo or --config is required')
-
- try:
- # Phase 1: Scrape GitHub repository
- scraper = GitHubScraper(config)
- scraper.scrape()
-
- if args.scrape_only:
- logger.info("Scrape complete (--scrape-only mode)")
- return
-
- # Phase 2: Build skill
- converter = GitHubToSkillConverter(config)
- converter.build_skill()
-
- logger.info(f"\n✅ Success! Skill created at: output/{config.get('name', config['repo'].split('/')[-1])}/")
- logger.info(f"Next step: skill-seekers-package output/{config.get('name', config['repo'].split('/')[-1])}/")
-
- except Exception as e:
- logger.error(f"Error: {e}")
- sys.exit(1)
-
-
-if __name__ == '__main__':
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_detector.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_detector.py
deleted file mode 100644
index 688fdb7..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_detector.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# ABOUTME: Detects and validates llms.txt file availability at documentation URLs
-# ABOUTME: Supports llms-full.txt, llms.txt, and llms-small.txt variants
-
-import requests
-from typing import Optional, Dict, List
-from urllib.parse import urlparse
-
-class LlmsTxtDetector:
- """Detect llms.txt files at documentation URLs"""
-
- VARIANTS = [
- ('llms-full.txt', 'full'),
- ('llms.txt', 'standard'),
- ('llms-small.txt', 'small')
- ]
-
- def __init__(self, base_url: str):
- self.base_url = base_url.rstrip('/')
-
- def detect(self) -> Optional[Dict[str, str]]:
- """
- Detect available llms.txt variant.
-
- Returns:
- Dict with 'url' and 'variant' keys, or None if not found
- """
- parsed = urlparse(self.base_url)
- root_url = f"{parsed.scheme}://{parsed.netloc}"
-
- for filename, variant in self.VARIANTS:
- url = f"{root_url}/{filename}"
-
- if self._check_url_exists(url):
- return {'url': url, 'variant': variant}
-
- return None
-
- def detect_all(self) -> List[Dict[str, str]]:
- """
- Detect all available llms.txt variants.
-
- Returns:
- List of dicts with 'url' and 'variant' keys for each found variant
- """
- found_variants = []
-
- for filename, variant in self.VARIANTS:
- parsed = urlparse(self.base_url)
- root_url = f"{parsed.scheme}://{parsed.netloc}"
- url = f"{root_url}/{filename}"
-
- if self._check_url_exists(url):
- found_variants.append({
- 'url': url,
- 'variant': variant
- })
-
- return found_variants
-
- def _check_url_exists(self, url: str) -> bool:
- """Check if URL returns 200 status"""
- try:
- response = requests.head(url, timeout=5, allow_redirects=True)
- return response.status_code == 200
- except requests.RequestException:
- return False
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_downloader.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_downloader.py
deleted file mode 100644
index 1049f86..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_downloader.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""ABOUTME: Downloads llms.txt files from documentation URLs with retry logic"""
-"""ABOUTME: Validates markdown content and handles timeouts with exponential backoff"""
-
-import requests
-import time
-from typing import Optional
-
-class LlmsTxtDownloader:
- """Download llms.txt content from URLs with retry logic"""
-
- def __init__(self, url: str, timeout: int = 30, max_retries: int = 3):
- self.url = url
- self.timeout = timeout
- self.max_retries = max_retries
-
- def get_proper_filename(self) -> str:
- """
- Extract filename from URL and convert .txt to .md
-
- Returns:
- Proper filename with .md extension
-
- Examples:
- https://hono.dev/llms-full.txt -> llms-full.md
- https://hono.dev/llms.txt -> llms.md
- https://hono.dev/llms-small.txt -> llms-small.md
- """
- # Extract filename from URL
- from urllib.parse import urlparse
- parsed = urlparse(self.url)
- filename = parsed.path.split('/')[-1]
-
- # Replace .txt with .md
- if filename.endswith('.txt'):
- filename = filename[:-4] + '.md'
-
- return filename
-
- def _is_markdown(self, content: str) -> bool:
- """
- Check if content looks like markdown.
-
- Returns:
- True if content contains markdown patterns
- """
- markdown_patterns = ['# ', '## ', '```', '- ', '* ', '`']
- return any(pattern in content for pattern in markdown_patterns)
-
- def download(self) -> Optional[str]:
- """
- Download llms.txt content with retry logic.
-
- Returns:
- String content or None if download fails
- """
- headers = {
- 'User-Agent': 'Skill-Seekers-llms.txt-Reader/1.0'
- }
-
- for attempt in range(self.max_retries):
- try:
- response = requests.get(
- self.url,
- headers=headers,
- timeout=self.timeout
- )
- response.raise_for_status()
-
- content = response.text
-
- # Validate content is not empty
- if len(content) < 100:
- print(f"⚠️ Content too short ({len(content)} chars), rejecting")
- return None
-
- # Validate content looks like markdown
- if not self._is_markdown(content):
- print(f"⚠️ Content doesn't look like markdown")
- return None
-
- return content
-
- except requests.RequestException as e:
- if attempt < self.max_retries - 1:
- # Calculate exponential backoff delay: 1s, 2s, 4s, etc.
- delay = 2 ** attempt
- print(f"⚠️ Attempt {attempt + 1}/{self.max_retries} failed: {e}")
- print(f" Retrying in {delay}s...")
- time.sleep(delay)
- else:
- print(f"❌ Failed to download {self.url} after {self.max_retries} attempts: {e}")
- return None
-
- return None
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_parser.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_parser.py
deleted file mode 100644
index e288c92..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_parser.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""ABOUTME: Parses llms.txt markdown content into structured page data"""
-"""ABOUTME: Extracts titles, content, code samples, and headings from markdown"""
-
-import re
-from typing import List, Dict
-
-class LlmsTxtParser:
- """Parse llms.txt markdown content into page structures"""
-
- def __init__(self, content: str):
- self.content = content
-
- def parse(self) -> List[Dict]:
- """
- Parse markdown content into page structures.
-
- Returns:
- List of page dicts with title, content, code_samples, headings
- """
- pages = []
-
- # Split by h1 headers (# Title)
- sections = re.split(r'\n# ', self.content)
-
- for section in sections:
- if not section.strip():
- continue
-
- # First line is title
- lines = section.split('\n')
- title = lines[0].strip('#').strip()
-
- # Parse content
- page = self._parse_section('\n'.join(lines[1:]), title)
- pages.append(page)
-
- return pages
-
- def _parse_section(self, content: str, title: str) -> Dict:
- """Parse a single section into page structure"""
- page = {
- 'title': title,
- 'content': '',
- 'code_samples': [],
- 'headings': [],
- 'url': f'llms-txt#{title.lower().replace(" ", "-")}',
- 'links': []
- }
-
- # Extract code blocks
- code_blocks = re.findall(r'```(\w+)?\n(.*?)```', content, re.DOTALL)
- for lang, code in code_blocks:
- page['code_samples'].append({
- 'code': code.strip(),
- 'language': lang or 'unknown'
- })
-
- # Extract h2/h3 headings
- headings = re.findall(r'^(#{2,3})\s+(.+)$', content, re.MULTILINE)
- for level_markers, text in headings:
- page['headings'].append({
- 'level': f'h{len(level_markers)}',
- 'text': text.strip(),
- 'id': text.lower().replace(' ', '-')
- })
-
- # Remove code blocks from content for plain text
- content_no_code = re.sub(r'```.*?```', '', content, flags=re.DOTALL)
-
- # Extract paragraphs
- paragraphs = [p.strip() for p in content_no_code.split('\n\n') if len(p.strip()) > 20]
- page['content'] = '\n\n'.join(paragraphs)
-
- return page
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/main.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/main.py
deleted file mode 100644
index dcf677d..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/main.py
+++ /dev/null
@@ -1,285 +0,0 @@
-#!/usr/bin/env python3
-"""
-Skill Seekers - Unified CLI Entry Point
-
-Provides a git-style unified command-line interface for all Skill Seekers tools.
-
-Usage:
- skill-seekers [options]
-
-Commands:
- scrape Scrape documentation website
- github Scrape GitHub repository
- pdf Extract from PDF file
- unified Multi-source scraping (docs + GitHub + PDF)
- enhance AI-powered enhancement (local, no API key)
- package Package skill into .zip file
- upload Upload skill to Claude
- estimate Estimate page count before scraping
-
-Examples:
- skill-seekers scrape --config configs/react.json
- skill-seekers github --repo microsoft/TypeScript
- skill-seekers unified --config configs/react_unified.json
- skill-seekers package output/react/
-"""
-
-import sys
-import argparse
-from typing import List, Optional
-
-
-def create_parser() -> argparse.ArgumentParser:
- """Create the main argument parser with subcommands."""
- parser = argparse.ArgumentParser(
- prog="skill-seekers",
- description="Convert documentation, GitHub repos, and PDFs into Claude AI skills",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Scrape documentation
- skill-seekers scrape --config configs/react.json
-
- # Scrape GitHub repository
- skill-seekers github --repo microsoft/TypeScript --name typescript
-
- # Multi-source scraping (unified)
- skill-seekers unified --config configs/react_unified.json
-
- # AI-powered enhancement
- skill-seekers enhance output/react/
-
- # Package and upload
- skill-seekers package output/react/
- skill-seekers upload output/react.zip
-
-For more information: https://github.com/yusufkaraaslan/Skill_Seekers
- """
- )
-
- parser.add_argument(
- "--version",
- action="version",
- version="%(prog)s 2.1.1"
- )
-
- subparsers = parser.add_subparsers(
- dest="command",
- title="commands",
- description="Available Skill Seekers commands",
- help="Command to run"
- )
-
- # === scrape subcommand ===
- scrape_parser = subparsers.add_parser(
- "scrape",
- help="Scrape documentation website",
- description="Scrape documentation website and generate skill"
- )
- scrape_parser.add_argument("--config", help="Config JSON file")
- scrape_parser.add_argument("--name", help="Skill name")
- scrape_parser.add_argument("--url", help="Documentation URL")
- scrape_parser.add_argument("--description", help="Skill description")
- scrape_parser.add_argument("--skip-scrape", action="store_true", help="Skip scraping, use cached data")
- scrape_parser.add_argument("--enhance", action="store_true", help="AI enhancement (API)")
- scrape_parser.add_argument("--enhance-local", action="store_true", help="AI enhancement (local)")
- scrape_parser.add_argument("--dry-run", action="store_true", help="Dry run mode")
- scrape_parser.add_argument("--async", dest="async_mode", action="store_true", help="Use async scraping")
- scrape_parser.add_argument("--workers", type=int, help="Number of async workers")
-
- # === github subcommand ===
- github_parser = subparsers.add_parser(
- "github",
- help="Scrape GitHub repository",
- description="Scrape GitHub repository and generate skill"
- )
- github_parser.add_argument("--config", help="Config JSON file")
- github_parser.add_argument("--repo", help="GitHub repo (owner/repo)")
- github_parser.add_argument("--name", help="Skill name")
- github_parser.add_argument("--description", help="Skill description")
-
- # === pdf subcommand ===
- pdf_parser = subparsers.add_parser(
- "pdf",
- help="Extract from PDF file",
- description="Extract content from PDF and generate skill"
- )
- pdf_parser.add_argument("--config", help="Config JSON file")
- pdf_parser.add_argument("--pdf", help="PDF file path")
- pdf_parser.add_argument("--name", help="Skill name")
- pdf_parser.add_argument("--description", help="Skill description")
- pdf_parser.add_argument("--from-json", help="Build from extracted JSON")
-
- # === unified subcommand ===
- unified_parser = subparsers.add_parser(
- "unified",
- help="Multi-source scraping (docs + GitHub + PDF)",
- description="Combine multiple sources into one skill"
- )
- unified_parser.add_argument("--config", required=True, help="Unified config JSON file")
- unified_parser.add_argument("--merge-mode", help="Merge mode (rule-based, claude-enhanced)")
- unified_parser.add_argument("--dry-run", action="store_true", help="Dry run mode")
-
- # === enhance subcommand ===
- enhance_parser = subparsers.add_parser(
- "enhance",
- help="AI-powered enhancement (local, no API key)",
- description="Enhance SKILL.md using Claude Code (local)"
- )
- enhance_parser.add_argument("skill_directory", help="Skill directory path")
-
- # === package subcommand ===
- package_parser = subparsers.add_parser(
- "package",
- help="Package skill into .zip file",
- description="Package skill directory into uploadable .zip"
- )
- package_parser.add_argument("skill_directory", help="Skill directory path")
- package_parser.add_argument("--no-open", action="store_true", help="Don't open output folder")
- package_parser.add_argument("--upload", action="store_true", help="Auto-upload after packaging")
-
- # === upload subcommand ===
- upload_parser = subparsers.add_parser(
- "upload",
- help="Upload skill to Claude",
- description="Upload .zip file to Claude via Anthropic API"
- )
- upload_parser.add_argument("zip_file", help=".zip file to upload")
- upload_parser.add_argument("--api-key", help="Anthropic API key")
-
- # === estimate subcommand ===
- estimate_parser = subparsers.add_parser(
- "estimate",
- help="Estimate page count before scraping",
- description="Estimate total pages for documentation scraping"
- )
- estimate_parser.add_argument("config", help="Config JSON file")
- estimate_parser.add_argument("--max-discovery", type=int, help="Max pages to discover")
-
- return parser
-
-
-def main(argv: Optional[List[str]] = None) -> int:
- """Main entry point for the unified CLI.
-
- Args:
- argv: Command-line arguments (defaults to sys.argv)
-
- Returns:
- Exit code (0 for success, non-zero for error)
- """
- parser = create_parser()
- args = parser.parse_args(argv)
-
- if not args.command:
- parser.print_help()
- return 1
-
- # Delegate to the appropriate tool
- try:
- if args.command == "scrape":
- from skill_seekers.cli.doc_scraper import main as scrape_main
- # Convert args namespace to sys.argv format for doc_scraper
- sys.argv = ["doc_scraper.py"]
- if args.config:
- sys.argv.extend(["--config", args.config])
- if args.name:
- sys.argv.extend(["--name", args.name])
- if args.url:
- sys.argv.extend(["--url", args.url])
- if args.description:
- sys.argv.extend(["--description", args.description])
- if args.skip_scrape:
- sys.argv.append("--skip-scrape")
- if args.enhance:
- sys.argv.append("--enhance")
- if args.enhance_local:
- sys.argv.append("--enhance-local")
- if args.dry_run:
- sys.argv.append("--dry-run")
- if args.async_mode:
- sys.argv.append("--async")
- if args.workers:
- sys.argv.extend(["--workers", str(args.workers)])
- return scrape_main() or 0
-
- elif args.command == "github":
- from skill_seekers.cli.github_scraper import main as github_main
- sys.argv = ["github_scraper.py"]
- if args.config:
- sys.argv.extend(["--config", args.config])
- if args.repo:
- sys.argv.extend(["--repo", args.repo])
- if args.name:
- sys.argv.extend(["--name", args.name])
- if args.description:
- sys.argv.extend(["--description", args.description])
- return github_main() or 0
-
- elif args.command == "pdf":
- from skill_seekers.cli.pdf_scraper import main as pdf_main
- sys.argv = ["pdf_scraper.py"]
- if args.config:
- sys.argv.extend(["--config", args.config])
- if args.pdf:
- sys.argv.extend(["--pdf", args.pdf])
- if args.name:
- sys.argv.extend(["--name", args.name])
- if args.description:
- sys.argv.extend(["--description", args.description])
- if args.from_json:
- sys.argv.extend(["--from-json", args.from_json])
- return pdf_main() or 0
-
- elif args.command == "unified":
- from skill_seekers.cli.unified_scraper import main as unified_main
- sys.argv = ["unified_scraper.py", "--config", args.config]
- if args.merge_mode:
- sys.argv.extend(["--merge-mode", args.merge_mode])
- if args.dry_run:
- sys.argv.append("--dry-run")
- return unified_main() or 0
-
- elif args.command == "enhance":
- from skill_seekers.cli.enhance_skill_local import main as enhance_main
- sys.argv = ["enhance_skill_local.py", args.skill_directory]
- return enhance_main() or 0
-
- elif args.command == "package":
- from skill_seekers.cli.package_skill import main as package_main
- sys.argv = ["package_skill.py", args.skill_directory]
- if args.no_open:
- sys.argv.append("--no-open")
- if args.upload:
- sys.argv.append("--upload")
- return package_main() or 0
-
- elif args.command == "upload":
- from skill_seekers.cli.upload_skill import main as upload_main
- sys.argv = ["upload_skill.py", args.zip_file]
- if args.api_key:
- sys.argv.extend(["--api-key", args.api_key])
- return upload_main() or 0
-
- elif args.command == "estimate":
- from skill_seekers.cli.estimate_pages import main as estimate_main
- sys.argv = ["estimate_pages.py", args.config]
- if args.max_discovery:
- sys.argv.extend(["--max-discovery", str(args.max_discovery)])
- return estimate_main() or 0
-
- else:
- print(f"Error: Unknown command '{args.command}'", file=sys.stderr)
- parser.print_help()
- return 1
-
- except KeyboardInterrupt:
- print("\n\nInterrupted by user", file=sys.stderr)
- return 130
- except Exception as e:
- print(f"Error: {e}", file=sys.stderr)
- return 1
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/merge_sources.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/merge_sources.py
deleted file mode 100644
index 552ac82..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/merge_sources.py
+++ /dev/null
@@ -1,513 +0,0 @@
-#!/usr/bin/env python3
-"""
-Source Merger for Multi-Source Skills
-
-Merges documentation and code data intelligently:
-- Rule-based merge: Fast, deterministic rules
-- Claude-enhanced merge: AI-powered reconciliation
-
-Handles conflicts and creates unified API reference.
-"""
-
-import json
-import logging
-import subprocess
-import tempfile
-import os
-from pathlib import Path
-from typing import Dict, List, Any, Optional
-from .conflict_detector import Conflict, ConflictDetector
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-class RuleBasedMerger:
- """
- Rule-based API merger using deterministic rules.
-
- Rules:
- 1. If API only in docs → Include with [DOCS_ONLY] tag
- 2. If API only in code → Include with [UNDOCUMENTED] tag
- 3. If both match perfectly → Include normally
- 4. If conflict → Include both versions with [CONFLICT] tag, prefer code signature
- """
-
- def __init__(self, docs_data: Dict, github_data: Dict, conflicts: List[Conflict]):
- """
- Initialize rule-based merger.
-
- Args:
- docs_data: Documentation scraper data
- github_data: GitHub scraper data
- conflicts: List of detected conflicts
- """
- self.docs_data = docs_data
- self.github_data = github_data
- self.conflicts = conflicts
-
- # Build conflict index for fast lookup
- self.conflict_index = {c.api_name: c for c in conflicts}
-
- # Extract APIs from both sources
- detector = ConflictDetector(docs_data, github_data)
- self.docs_apis = detector.docs_apis
- self.code_apis = detector.code_apis
-
- def merge_all(self) -> Dict[str, Any]:
- """
- Merge all APIs using rule-based logic.
-
- Returns:
- Dict containing merged API data
- """
- logger.info("Starting rule-based merge...")
-
- merged_apis = {}
-
- # Get all unique API names
- all_api_names = set(self.docs_apis.keys()) | set(self.code_apis.keys())
-
- for api_name in sorted(all_api_names):
- merged_api = self._merge_single_api(api_name)
- merged_apis[api_name] = merged_api
-
- logger.info(f"Merged {len(merged_apis)} APIs")
-
- return {
- 'merge_mode': 'rule-based',
- 'apis': merged_apis,
- 'summary': {
- 'total_apis': len(merged_apis),
- 'docs_only': sum(1 for api in merged_apis.values() if api['status'] == 'docs_only'),
- 'code_only': sum(1 for api in merged_apis.values() if api['status'] == 'code_only'),
- 'matched': sum(1 for api in merged_apis.values() if api['status'] == 'matched'),
- 'conflict': sum(1 for api in merged_apis.values() if api['status'] == 'conflict')
- }
- }
-
- def _merge_single_api(self, api_name: str) -> Dict[str, Any]:
- """
- Merge a single API using rules.
-
- Args:
- api_name: Name of the API to merge
-
- Returns:
- Merged API dict
- """
- in_docs = api_name in self.docs_apis
- in_code = api_name in self.code_apis
- has_conflict = api_name in self.conflict_index
-
- # Rule 1: Only in docs
- if in_docs and not in_code:
- conflict = self.conflict_index.get(api_name)
- return {
- 'name': api_name,
- 'status': 'docs_only',
- 'source': 'documentation',
- 'data': self.docs_apis[api_name],
- 'warning': 'This API is documented but not found in codebase',
- 'conflict': conflict.__dict__ if conflict else None
- }
-
- # Rule 2: Only in code
- if in_code and not in_docs:
- is_private = api_name.startswith('_')
- conflict = self.conflict_index.get(api_name)
- return {
- 'name': api_name,
- 'status': 'code_only',
- 'source': 'code',
- 'data': self.code_apis[api_name],
- 'warning': 'This API exists in code but is not documented' if not is_private else 'Internal/private API',
- 'conflict': conflict.__dict__ if conflict else None
- }
-
- # Both exist - check for conflicts
- docs_info = self.docs_apis[api_name]
- code_info = self.code_apis[api_name]
-
- # Rule 3: Both match perfectly (no conflict)
- if not has_conflict:
- return {
- 'name': api_name,
- 'status': 'matched',
- 'source': 'both',
- 'docs_data': docs_info,
- 'code_data': code_info,
- 'merged_signature': self._create_merged_signature(code_info, docs_info),
- 'merged_description': docs_info.get('docstring') or code_info.get('docstring')
- }
-
- # Rule 4: Conflict exists - prefer code signature, keep docs description
- conflict = self.conflict_index[api_name]
-
- return {
- 'name': api_name,
- 'status': 'conflict',
- 'source': 'both',
- 'docs_data': docs_info,
- 'code_data': code_info,
- 'conflict': conflict.__dict__,
- 'resolution': 'prefer_code_signature',
- 'merged_signature': self._create_merged_signature(code_info, docs_info),
- 'merged_description': docs_info.get('docstring') or code_info.get('docstring'),
- 'warning': conflict.difference
- }
-
- def _create_merged_signature(self, code_info: Dict, docs_info: Dict) -> str:
- """
- Create merged signature preferring code data.
-
- Args:
- code_info: API info from code
- docs_info: API info from docs
-
- Returns:
- Merged signature string
- """
- name = code_info.get('name', docs_info.get('name'))
- params = code_info.get('parameters', docs_info.get('parameters', []))
- return_type = code_info.get('return_type', docs_info.get('return_type'))
-
- # Build parameter string
- param_strs = []
- for param in params:
- param_str = param['name']
- if param.get('type_hint'):
- param_str += f": {param['type_hint']}"
- if param.get('default'):
- param_str += f" = {param['default']}"
- param_strs.append(param_str)
-
- signature = f"{name}({', '.join(param_strs)})"
-
- if return_type:
- signature += f" -> {return_type}"
-
- return signature
-
-
-class ClaudeEnhancedMerger:
- """
- Claude-enhanced API merger using local Claude Code.
-
- Opens Claude Code in a new terminal to intelligently reconcile conflicts.
- Uses the same approach as enhance_skill_local.py.
- """
-
- def __init__(self, docs_data: Dict, github_data: Dict, conflicts: List[Conflict]):
- """
- Initialize Claude-enhanced merger.
-
- Args:
- docs_data: Documentation scraper data
- github_data: GitHub scraper data
- conflicts: List of detected conflicts
- """
- self.docs_data = docs_data
- self.github_data = github_data
- self.conflicts = conflicts
-
- # First do rule-based merge as baseline
- self.rule_merger = RuleBasedMerger(docs_data, github_data, conflicts)
-
- def merge_all(self) -> Dict[str, Any]:
- """
- Merge all APIs using Claude enhancement.
-
- Returns:
- Dict containing merged API data
- """
- logger.info("Starting Claude-enhanced merge...")
-
- # Create temporary workspace
- workspace_dir = self._create_workspace()
-
- # Launch Claude Code for enhancement
- logger.info("Launching Claude Code for intelligent merging...")
- logger.info("Claude will analyze conflicts and create reconciled API reference")
-
- try:
- self._launch_claude_merge(workspace_dir)
-
- # Read enhanced results
- merged_data = self._read_merged_results(workspace_dir)
-
- logger.info("Claude-enhanced merge complete")
- return merged_data
-
- except Exception as e:
- logger.error(f"Claude enhancement failed: {e}")
- logger.info("Falling back to rule-based merge")
- return self.rule_merger.merge_all()
-
- def _create_workspace(self) -> str:
- """
- Create temporary workspace with merge context.
-
- Returns:
- Path to workspace directory
- """
- workspace = tempfile.mkdtemp(prefix='skill_merge_')
- logger.info(f"Created merge workspace: {workspace}")
-
- # Write context files for Claude
- self._write_context_files(workspace)
-
- return workspace
-
- def _write_context_files(self, workspace: str):
- """Write context files for Claude to analyze."""
-
- # 1. Write conflicts summary
- conflicts_file = os.path.join(workspace, 'conflicts.json')
- with open(conflicts_file, 'w') as f:
- json.dump({
- 'conflicts': [c.__dict__ for c in self.conflicts],
- 'summary': {
- 'total': len(self.conflicts),
- 'by_type': self._count_by_field('type'),
- 'by_severity': self._count_by_field('severity')
- }
- }, f, indent=2)
-
- # 2. Write documentation APIs
- docs_apis_file = os.path.join(workspace, 'docs_apis.json')
- detector = ConflictDetector(self.docs_data, self.github_data)
- with open(docs_apis_file, 'w') as f:
- json.dump(detector.docs_apis, f, indent=2)
-
- # 3. Write code APIs
- code_apis_file = os.path.join(workspace, 'code_apis.json')
- with open(code_apis_file, 'w') as f:
- json.dump(detector.code_apis, f, indent=2)
-
- # 4. Write merge instructions for Claude
- instructions = """# API Merge Task
-
-You are merging API documentation from two sources:
-1. Official documentation (user-facing)
-2. Source code analysis (implementation reality)
-
-## Context Files:
-- `conflicts.json` - All detected conflicts between sources
-- `docs_apis.json` - APIs from documentation
-- `code_apis.json` - APIs from source code
-
-## Your Task:
-For each conflict, reconcile the differences intelligently:
-
-1. **Prefer code signatures as source of truth**
- - Use actual parameter names, types, defaults from code
- - Code is what actually runs, docs might be outdated
-
-2. **Keep documentation descriptions**
- - Docs are user-friendly, code comments might be technical
- - Keep the docs' explanation of what the API does
-
-3. **Add implementation notes for discrepancies**
- - If docs differ from code, explain the difference
- - Example: "⚠️ The `snap` parameter exists in code but is not documented"
-
-4. **Flag missing APIs clearly**
- - Missing in docs → Add [UNDOCUMENTED] tag
- - Missing in code → Add [REMOVED] or [DOCS_ERROR] tag
-
-5. **Create unified API reference**
- - One definitive signature per API
- - Clear warnings about conflicts
- - Implementation notes where helpful
-
-## Output Format:
-Create `merged_apis.json` with this structure:
-
-```json
-{
- "apis": {
- "API.name": {
- "signature": "final_signature_here",
- "parameters": [...],
- "return_type": "type",
- "description": "user-friendly description",
- "implementation_notes": "Any discrepancies or warnings",
- "source": "both|docs_only|code_only",
- "confidence": "high|medium|low"
- }
- }
-}
-```
-
-Take your time to analyze each conflict carefully. The goal is to create the most accurate and helpful API reference possible.
-"""
-
- instructions_file = os.path.join(workspace, 'MERGE_INSTRUCTIONS.md')
- with open(instructions_file, 'w') as f:
- f.write(instructions)
-
- logger.info(f"Wrote context files to {workspace}")
-
- def _count_by_field(self, field: str) -> Dict[str, int]:
- """Count conflicts by a specific field."""
- counts = {}
- for conflict in self.conflicts:
- value = getattr(conflict, field)
- counts[value] = counts.get(value, 0) + 1
- return counts
-
- def _launch_claude_merge(self, workspace: str):
- """
- Launch Claude Code to perform merge.
-
- Similar to enhance_skill_local.py approach.
- """
- # Create a script that Claude will execute
- script_path = os.path.join(workspace, 'merge_script.sh')
-
- script_content = f"""#!/bin/bash
-# Automatic merge script for Claude Code
-
-cd "{workspace}"
-
-echo "📊 Analyzing conflicts..."
-cat conflicts.json | head -20
-
-echo ""
-echo "📖 Documentation APIs: $(cat docs_apis.json | grep -c '\"name\"')"
-echo "💻 Code APIs: $(cat code_apis.json | grep -c '\"name\"')"
-echo ""
-echo "Please review the conflicts and create merged_apis.json"
-echo "Follow the instructions in MERGE_INSTRUCTIONS.md"
-echo ""
-echo "When done, save merged_apis.json and close this terminal."
-
-# Wait for user to complete merge
-read -p "Press Enter when merge is complete..."
-"""
-
- with open(script_path, 'w') as f:
- f.write(script_content)
-
- os.chmod(script_path, 0o755)
-
- # Open new terminal with Claude Code
- # Try different terminal emulators
- terminals = [
- ['x-terminal-emulator', '-e'],
- ['gnome-terminal', '--'],
- ['xterm', '-e'],
- ['konsole', '-e']
- ]
-
- for terminal_cmd in terminals:
- try:
- cmd = terminal_cmd + ['bash', script_path]
- subprocess.Popen(cmd)
- logger.info(f"Opened terminal with {terminal_cmd[0]}")
- break
- except FileNotFoundError:
- continue
-
- # Wait for merge to complete
- merged_file = os.path.join(workspace, 'merged_apis.json')
- logger.info(f"Waiting for merged results at: {merged_file}")
- logger.info("Close the terminal when done to continue...")
-
- # Poll for file existence
- import time
- timeout = 3600 # 1 hour max
- elapsed = 0
- while not os.path.exists(merged_file) and elapsed < timeout:
- time.sleep(5)
- elapsed += 5
-
- if not os.path.exists(merged_file):
- raise TimeoutError("Claude merge timed out after 1 hour")
-
- def _read_merged_results(self, workspace: str) -> Dict[str, Any]:
- """Read merged results from workspace."""
- merged_file = os.path.join(workspace, 'merged_apis.json')
-
- if not os.path.exists(merged_file):
- raise FileNotFoundError(f"Merged results not found: {merged_file}")
-
- with open(merged_file, 'r') as f:
- merged_data = json.load(f)
-
- return {
- 'merge_mode': 'claude-enhanced',
- **merged_data
- }
-
-
-def merge_sources(docs_data_path: str,
- github_data_path: str,
- output_path: str,
- mode: str = 'rule-based') -> Dict[str, Any]:
- """
- Merge documentation and GitHub data.
-
- Args:
- docs_data_path: Path to documentation data JSON
- github_data_path: Path to GitHub data JSON
- output_path: Path to save merged output
- mode: 'rule-based' or 'claude-enhanced'
-
- Returns:
- Merged data dict
- """
- # Load data
- with open(docs_data_path, 'r') as f:
- docs_data = json.load(f)
-
- with open(github_data_path, 'r') as f:
- github_data = json.load(f)
-
- # Detect conflicts
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector.detect_all_conflicts()
-
- logger.info(f"Detected {len(conflicts)} conflicts")
-
- # Merge based on mode
- if mode == 'claude-enhanced':
- merger = ClaudeEnhancedMerger(docs_data, github_data, conflicts)
- else:
- merger = RuleBasedMerger(docs_data, github_data, conflicts)
-
- merged_data = merger.merge_all()
-
- # Save merged data
- with open(output_path, 'w') as f:
- json.dump(merged_data, f, indent=2, ensure_ascii=False)
-
- logger.info(f"Merged data saved to: {output_path}")
-
- return merged_data
-
-
-if __name__ == '__main__':
- import argparse
-
- parser = argparse.ArgumentParser(description='Merge documentation and code sources')
- parser.add_argument('docs_data', help='Path to documentation data JSON')
- parser.add_argument('github_data', help='Path to GitHub data JSON')
- parser.add_argument('--output', '-o', default='merged_data.json', help='Output file path')
- parser.add_argument('--mode', '-m', choices=['rule-based', 'claude-enhanced'],
- default='rule-based', help='Merge mode')
-
- args = parser.parse_args()
-
- merged = merge_sources(args.docs_data, args.github_data, args.output, args.mode)
-
- # Print summary
- summary = merged.get('summary', {})
- print(f"\n✅ Merge complete ({merged.get('merge_mode')})")
- print(f" Total APIs: {summary.get('total_apis', 0)}")
- print(f" Matched: {summary.get('matched', 0)}")
- print(f" Docs only: {summary.get('docs_only', 0)}")
- print(f" Code only: {summary.get('code_only', 0)}")
- print(f" Conflicts: {summary.get('conflict', 0)}")
- print(f"\n📄 Saved to: {args.output}")
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/package_multi.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/package_multi.py
deleted file mode 100644
index bffdb9c..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/package_multi.py
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env python3
-"""
-Multi-Skill Packager
-
-Package multiple skills at once. Useful for packaging router + sub-skills together.
-"""
-
-import sys
-import argparse
-from pathlib import Path
-import subprocess
-
-
-def package_skill(skill_dir: Path) -> bool:
- """Package a single skill"""
- try:
- result = subprocess.run(
- [sys.executable, str(Path(__file__).parent / "package_skill.py"), str(skill_dir)],
- capture_output=True,
- text=True
- )
- return result.returncode == 0
- except Exception as e:
- print(f"❌ Error packaging {skill_dir}: {e}")
- return False
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Package multiple skills at once",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Package all godot skills
- python3 package_multi.py output/godot*/
-
- # Package specific skills
- python3 package_multi.py output/godot-2d/ output/godot-3d/ output/godot-scripting/
- """
- )
-
- parser.add_argument(
- 'skill_dirs',
- nargs='+',
- help='Skill directories to package'
- )
-
- args = parser.parse_args()
-
- print(f"\n{'='*60}")
- print(f"MULTI-SKILL PACKAGER")
- print(f"{'='*60}\n")
-
- skill_dirs = [Path(d) for d in args.skill_dirs]
- success_count = 0
- total_count = len(skill_dirs)
-
- for skill_dir in skill_dirs:
- if not skill_dir.exists():
- print(f"⚠️ Skipping (not found): {skill_dir}")
- continue
-
- if not (skill_dir / "SKILL.md").exists():
- print(f"⚠️ Skipping (no SKILL.md): {skill_dir}")
- continue
-
- print(f"📦 Packaging: {skill_dir.name}")
- if package_skill(skill_dir):
- success_count += 1
- print(f" ✅ Success")
- else:
- print(f" ❌ Failed")
- print("")
-
- print(f"{'='*60}")
- print(f"SUMMARY: {success_count}/{total_count} skills packaged")
- print(f"{'='*60}\n")
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/package_skill.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/package_skill.py
deleted file mode 100644
index cf251d0..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/package_skill.py
+++ /dev/null
@@ -1,220 +0,0 @@
-#!/usr/bin/env python3
-"""
-Simple Skill Packager
-Packages a skill directory into a .zip file for Claude.
-
-Usage:
- skill-seekers package output/steam-inventory/
- skill-seekers package output/react/
- skill-seekers package output/react/ --no-open # Don't open folder
-"""
-
-import os
-import sys
-import zipfile
-import argparse
-from pathlib import Path
-
-# Import utilities
-try:
- from utils import (
- open_folder,
- print_upload_instructions,
- format_file_size,
- validate_skill_directory
- )
- from quality_checker import SkillQualityChecker, print_report
-except ImportError:
- # If running from different directory, add cli to path
- sys.path.insert(0, str(Path(__file__).parent))
- from utils import (
- open_folder,
- print_upload_instructions,
- format_file_size,
- validate_skill_directory
- )
- from quality_checker import SkillQualityChecker, print_report
-
-
-def package_skill(skill_dir, open_folder_after=True, skip_quality_check=False):
- """
- Package a skill directory into a .zip file
-
- Args:
- skill_dir: Path to skill directory
- open_folder_after: Whether to open the output folder after packaging
- skip_quality_check: Skip quality checks before packaging
-
- Returns:
- tuple: (success, zip_path) where success is bool and zip_path is Path or None
- """
- skill_path = Path(skill_dir)
-
- # Validate skill directory
- is_valid, error_msg = validate_skill_directory(skill_path)
- if not is_valid:
- print(f"❌ Error: {error_msg}")
- return False, None
-
- # Run quality checks (unless skipped)
- if not skip_quality_check:
- print("\n" + "=" * 60)
- print("QUALITY CHECK")
- print("=" * 60)
-
- checker = SkillQualityChecker(skill_path)
- report = checker.check_all()
-
- # Print report
- print_report(report, verbose=False)
-
- # If there are errors or warnings, ask user to confirm
- if report.has_errors or report.has_warnings:
- print("=" * 60)
- response = input("\nContinue with packaging? (y/n): ").strip().lower()
- if response != 'y':
- print("\n❌ Packaging cancelled by user")
- return False, None
- print()
- else:
- print("=" * 60)
- print()
-
- # Create zip filename
- skill_name = skill_path.name
- zip_path = skill_path.parent / f"{skill_name}.zip"
-
- print(f"📦 Packaging skill: {skill_name}")
- print(f" Source: {skill_path}")
- print(f" Output: {zip_path}")
-
- # Create zip file
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
- for root, dirs, files in os.walk(skill_path):
- # Skip backup files
- files = [f for f in files if not f.endswith('.backup')]
-
- for file in files:
- file_path = Path(root) / file
- arcname = file_path.relative_to(skill_path)
- zf.write(file_path, arcname)
- print(f" + {arcname}")
-
- # Get zip size
- zip_size = zip_path.stat().st_size
- print(f"\n✅ Package created: {zip_path}")
- print(f" Size: {zip_size:,} bytes ({format_file_size(zip_size)})")
-
- # Open folder in file browser
- if open_folder_after:
- print(f"\n📂 Opening folder: {zip_path.parent}")
- open_folder(zip_path.parent)
-
- # Print upload instructions
- print_upload_instructions(zip_path)
-
- return True, zip_path
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Package a skill directory into a .zip file for Claude",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Package skill with quality checks (recommended)
- skill-seekers package output/react/
-
- # Package skill without opening folder
- skill-seekers package output/react/ --no-open
-
- # Skip quality checks (faster, but not recommended)
- skill-seekers package output/react/ --skip-quality-check
-
- # Package and auto-upload to Claude
- skill-seekers package output/react/ --upload
-
- # Get help
- skill-seekers package --help
- """
- )
-
- parser.add_argument(
- 'skill_dir',
- help='Path to skill directory (e.g., output/react/)'
- )
-
- parser.add_argument(
- '--no-open',
- action='store_true',
- help='Do not open the output folder after packaging'
- )
-
- parser.add_argument(
- '--skip-quality-check',
- action='store_true',
- help='Skip quality checks before packaging'
- )
-
- parser.add_argument(
- '--upload',
- action='store_true',
- help='Automatically upload to Claude after packaging (requires ANTHROPIC_API_KEY)'
- )
-
- args = parser.parse_args()
-
- success, zip_path = package_skill(
- args.skill_dir,
- open_folder_after=not args.no_open,
- skip_quality_check=args.skip_quality_check
- )
-
- if not success:
- sys.exit(1)
-
- # Auto-upload if requested
- if args.upload:
- # Check if API key is set BEFORE attempting upload
- api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
-
- if not api_key:
- # No API key - show helpful message but DON'T fail
- print("\n" + "="*60)
- print("💡 Automatic Upload")
- print("="*60)
- print()
- print("To enable automatic upload:")
- print(" 1. Get API key from https://console.anthropic.com/")
- print(" 2. Set: export ANTHROPIC_API_KEY=sk-ant-...")
- print(" 3. Run package_skill.py with --upload flag")
- print()
- print("For now, use manual upload (instructions above) ☝️")
- print("="*60)
- # Exit successfully - packaging worked!
- sys.exit(0)
-
- # API key exists - try upload
- try:
- from upload_skill import upload_skill_api
- print("\n" + "="*60)
- upload_success, message = upload_skill_api(zip_path)
- if not upload_success:
- print(f"❌ Upload failed: {message}")
- print()
- print("💡 Try manual upload instead (instructions above) ☝️")
- print("="*60)
- # Exit successfully - packaging worked even if upload failed
- sys.exit(0)
- else:
- print("="*60)
- sys.exit(0)
- except ImportError:
- print("\n❌ Error: upload_skill.py not found")
- sys.exit(1)
-
- sys.exit(0)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/pdf_extractor_poc.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/pdf_extractor_poc.py
deleted file mode 100644
index f8c0fe8..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/pdf_extractor_poc.py
+++ /dev/null
@@ -1,1222 +0,0 @@
-#!/usr/bin/env python3
-"""
-PDF Text Extractor - Complete Feature Set (Tasks B1.2 + B1.3 + B1.4 + B1.5 + Priority 2 & 3)
-
-Extracts text, code blocks, and images from PDF documentation files.
-Uses PyMuPDF (fitz) for fast, high-quality extraction.
-
-Features:
- - Text and markdown extraction
- - Code block detection (font, indent, pattern)
- - Language detection with confidence scoring (19+ languages) (B1.4)
- - Syntax validation and quality scoring (B1.4)
- - Quality statistics and filtering (B1.4)
- - Image extraction to files (B1.5)
- - Image filtering by size (B1.5)
- - Page chunking and chapter detection (B1.3)
- - Code block merging across pages (B1.3)
-
-Advanced Features (Priority 2 & 3):
- - OCR support for scanned PDFs (requires pytesseract) (Priority 2)
- - Password-protected PDF support (Priority 2)
- - Table extraction (Priority 2)
- - Parallel page processing (Priority 3)
- - Caching of expensive operations (Priority 3)
-
-Usage:
- # Basic extraction
- python3 pdf_extractor_poc.py input.pdf
- python3 pdf_extractor_poc.py input.pdf --output output.json
- python3 pdf_extractor_poc.py input.pdf --verbose
-
- # Quality filtering
- python3 pdf_extractor_poc.py input.pdf --min-quality 5.0
-
- # Image extraction
- python3 pdf_extractor_poc.py input.pdf --extract-images
- python3 pdf_extractor_poc.py input.pdf --extract-images --image-dir images/
-
- # Advanced features
- python3 pdf_extractor_poc.py scanned.pdf --ocr
- python3 pdf_extractor_poc.py encrypted.pdf --password mypassword
- python3 pdf_extractor_poc.py input.pdf --extract-tables
- python3 pdf_extractor_poc.py large.pdf --parallel --workers 8
-
-Example:
- python3 pdf_extractor_poc.py docs/manual.pdf -o output.json -v \
- --chunk-size 15 --min-quality 6.0 --extract-images \
- --extract-tables --parallel
-"""
-
-import os
-import sys
-import json
-import re
-import argparse
-from pathlib import Path
-
-# Check if PyMuPDF is installed
-try:
- import fitz # PyMuPDF
-except ImportError:
- print("ERROR: PyMuPDF not installed")
- print("Install with: pip install PyMuPDF")
- sys.exit(1)
-
-# Optional dependencies for advanced features
-try:
- import pytesseract
- from PIL import Image
- TESSERACT_AVAILABLE = True
-except ImportError:
- TESSERACT_AVAILABLE = False
-
-try:
- import concurrent.futures
- CONCURRENT_AVAILABLE = True
-except ImportError:
- CONCURRENT_AVAILABLE = False
-
-
-class PDFExtractor:
- """Extract text and code from PDF documentation"""
-
- def __init__(self, pdf_path, verbose=False, chunk_size=10, min_quality=0.0,
- extract_images=False, image_dir=None, min_image_size=100,
- use_ocr=False, password=None, extract_tables=False,
- parallel=False, max_workers=None, use_cache=True):
- self.pdf_path = pdf_path
- self.verbose = verbose
- self.chunk_size = chunk_size # Pages per chunk (0 = no chunking)
- self.min_quality = min_quality # Minimum quality score (0-10)
- self.extract_images = extract_images # Extract images to files (NEW in B1.5)
- self.image_dir = image_dir # Directory to save images (NEW in B1.5)
- self.min_image_size = min_image_size # Minimum image dimension (NEW in B1.5)
-
- # Advanced features (Priority 2 & 3)
- self.use_ocr = use_ocr # OCR for scanned PDFs (Priority 2)
- self.password = password # Password for encrypted PDFs (Priority 2)
- self.extract_tables = extract_tables # Extract tables (Priority 2)
- self.parallel = parallel # Parallel processing (Priority 3)
- self.max_workers = max_workers or os.cpu_count() # Worker threads (Priority 3)
- self.use_cache = use_cache # Cache expensive operations (Priority 3)
-
- self.doc = None
- self.pages = []
- self.chapters = [] # Detected chapters/sections
- self.extracted_images = [] # List of extracted image info (NEW in B1.5)
- self._cache = {} # Cache for expensive operations (Priority 3)
-
- def log(self, message):
- """Print message if verbose mode enabled"""
- if self.verbose:
- print(message)
-
- def extract_text_with_ocr(self, page):
- """
- Extract text from scanned PDF page using OCR (Priority 2).
- Falls back to regular text extraction if OCR is not available.
-
- Args:
- page: PyMuPDF page object
-
- Returns:
- str: Extracted text
- """
- # Try regular text extraction first
- text = page.get_text("text").strip()
-
- # If page has very little text, it might be scanned
- if len(text) < 50 and self.use_ocr:
- if not TESSERACT_AVAILABLE:
- self.log("⚠️ OCR requested but pytesseract not installed")
- self.log(" Install with: pip install pytesseract Pillow")
- return text
-
- try:
- # Render page as image
- pix = page.get_pixmap()
- img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
-
- # Run OCR
- ocr_text = pytesseract.image_to_string(img)
- self.log(f" OCR extracted {len(ocr_text)} chars (was {len(text)})")
- return ocr_text if len(ocr_text) > len(text) else text
-
- except Exception as e:
- self.log(f" OCR failed: {e}")
- return text
-
- return text
-
- def extract_tables_from_page(self, page):
- """
- Extract tables from PDF page (Priority 2).
- Uses PyMuPDF's table detection.
-
- Args:
- page: PyMuPDF page object
-
- Returns:
- list: List of extracted tables as dicts
- """
- if not self.extract_tables:
- return []
-
- tables = []
- try:
- # PyMuPDF table extraction
- tabs = page.find_tables()
- for idx, tab in enumerate(tabs.tables):
- table_data = {
- 'table_index': idx,
- 'rows': tab.extract(),
- 'bbox': tab.bbox,
- 'row_count': len(tab.extract()),
- 'col_count': len(tab.extract()[0]) if tab.extract() else 0
- }
- tables.append(table_data)
- self.log(f" Found table {idx}: {table_data['row_count']}x{table_data['col_count']}")
-
- except Exception as e:
- self.log(f" Table extraction failed: {e}")
-
- return tables
-
- def get_cached(self, key):
- """
- Get cached value (Priority 3).
-
- Args:
- key: Cache key
-
- Returns:
- Cached value or None
- """
- if not self.use_cache:
- return None
- return self._cache.get(key)
-
- def set_cached(self, key, value):
- """
- Set cached value (Priority 3).
-
- Args:
- key: Cache key
- value: Value to cache
- """
- if self.use_cache:
- self._cache[key] = value
-
- def detect_language_from_code(self, code):
- """
- Detect programming language from code content using patterns.
- Enhanced in B1.4 with confidence scoring.
-
- Returns (language, confidence) tuple
- """
- code_lower = code.lower()
-
- # Language detection patterns with weights
- patterns = {
- 'python': [
- (r'\bdef\s+\w+\s*\(', 3),
- (r'\bimport\s+\w+', 2),
- (r'\bclass\s+\w+:', 3),
- (r'\bfrom\s+\w+\s+import', 2),
- (r':\s*$', 1), # Lines ending with :
- (r'^\s{4}|\t', 1), # Indentation
- ],
- 'javascript': [
- (r'\bfunction\s+\w+\s*\(', 3),
- (r'\bconst\s+\w+\s*=', 2),
- (r'\blet\s+\w+\s*=', 2),
- (r'=>', 2),
- (r'\bconsole\.log', 2),
- (r'\bvar\s+\w+\s*=', 1),
- ],
- 'java': [
- (r'\bpublic\s+class\s+\w+', 4),
- (r'\bprivate\s+\w+\s+\w+', 2),
- (r'\bSystem\.out\.println', 3),
- (r'\bpublic\s+static\s+void', 3),
- ],
- 'cpp': [
- (r'#include\s*<', 3),
- (r'\bstd::', 3),
- (r'\bnamespace\s+\w+', 2),
- (r'cout\s*<<', 3),
- (r'\bvoid\s+\w+\s*\(', 1),
- ],
- 'c': [
- (r'#include\s+<\w+\.h>', 4),
- (r'\bprintf\s*\(', 3),
- (r'\bmain\s*\(', 2),
- (r'\bstruct\s+\w+', 2),
- ],
- 'csharp': [
- (r'\bnamespace\s+\w+', 3),
- (r'\bpublic\s+class\s+\w+', 3),
- (r'\busing\s+System', 3),
- ],
- 'go': [
- (r'\bfunc\s+\w+\s*\(', 3),
- (r'\bpackage\s+\w+', 4),
- (r':=', 2),
- (r'\bfmt\.Print', 2),
- ],
- 'rust': [
- (r'\bfn\s+\w+\s*\(', 4),
- (r'\blet\s+mut\s+\w+', 3),
- (r'\bprintln!', 3),
- (r'\bimpl\s+\w+', 2),
- ],
- 'php': [
- (r'<\?php', 5),
- (r'\$\w+\s*=', 2),
- (r'\bfunction\s+\w+\s*\(', 1),
- ],
- 'ruby': [
- (r'\bdef\s+\w+', 3),
- (r'\bend\b', 2),
- (r'\brequire\s+[\'"]', 2),
- ],
- 'swift': [
- (r'\bfunc\s+\w+\s*\(', 3),
- (r'\bvar\s+\w+:', 2),
- (r'\blet\s+\w+:', 2),
- ],
- 'kotlin': [
- (r'\bfun\s+\w+\s*\(', 4),
- (r'\bval\s+\w+\s*=', 2),
- (r'\bvar\s+\w+\s*=', 2),
- ],
- 'shell': [
- (r'#!/bin/bash', 5),
- (r'#!/bin/sh', 5),
- (r'\becho\s+', 1),
- (r'\$\{?\w+\}?', 1),
- ],
- 'sql': [
- (r'\bSELECT\s+', 4),
- (r'\bFROM\s+', 3),
- (r'\bWHERE\s+', 2),
- (r'\bINSERT\s+INTO', 4),
- (r'\bCREATE\s+TABLE', 4),
- ],
- 'html': [
- (r'', 1),
- ],
- }
-
- # Calculate confidence scores for each language
- scores = {}
- for lang, lang_patterns in patterns.items():
- score = 0
- for pattern, weight in lang_patterns:
- if re.search(pattern, code, re.IGNORECASE | re.MULTILINE):
- score += weight
- if score > 0:
- scores[lang] = score
-
- if not scores:
- return 'unknown', 0
-
- # Get language with highest score
- best_lang = max(scores, key=scores.get)
- confidence = min(scores[best_lang] / 10.0, 1.0) # Normalize to 0-1
-
- return best_lang, confidence
-
- def validate_code_syntax(self, code, language):
- """
- Validate code syntax (basic checks).
- Enhanced in B1.4 with syntax validation.
-
- Returns (is_valid, issues) tuple
- """
- issues = []
-
- # Common syntax checks
- if not code.strip():
- return False, ['Empty code block']
-
- # Language-specific validation
- if language == 'python':
- # Check indentation consistency
- lines = code.split('\n')
- indent_chars = set()
- for line in lines:
- if line.startswith(' '):
- indent_chars.add('space')
- elif line.startswith('\t'):
- indent_chars.add('tab')
-
- if len(indent_chars) > 1:
- issues.append('Mixed tabs and spaces')
-
- # Check for unclosed brackets/parens
- open_count = code.count('(') + code.count('[') + code.count('{')
- close_count = code.count(')') + code.count(']') + code.count('}')
- if abs(open_count - close_count) > 2: # Allow small mismatch
- issues.append('Unbalanced brackets')
-
- elif language in ['javascript', 'java', 'cpp', 'c', 'csharp', 'go']:
- # Check for balanced braces
- open_braces = code.count('{')
- close_braces = code.count('}')
- if abs(open_braces - close_braces) > 1:
- issues.append('Unbalanced braces')
-
- elif language == 'json':
- # Try to parse JSON
- try:
- json.loads(code)
- except (json.JSONDecodeError, ValueError) as e:
- issues.append(f'Invalid JSON syntax: {str(e)[:50]}')
-
- # General checks
- # Check if code looks like natural language (too many common words)
- common_words = ['the', 'and', 'for', 'with', 'this', 'that', 'have', 'from']
- word_count = sum(1 for word in common_words if word in code.lower())
- if word_count > 5 and len(code.split()) < 50:
- issues.append('May be natural language, not code')
-
- # Check code/comment ratio
- comment_lines = sum(1 for line in code.split('\n') if line.strip().startswith(('#', '//', '/*', '*', '--')))
- total_lines = len([l for l in code.split('\n') if l.strip()])
- if total_lines > 0 and comment_lines / total_lines > 0.7:
- issues.append('Mostly comments')
-
- return len(issues) == 0, issues
-
- def score_code_quality(self, code, language, confidence):
- """
- Score the quality/usefulness of detected code block.
- New in B1.4.
-
- Returns quality score (0-10)
- """
- score = 5.0 # Start with neutral score
-
- # Factor 1: Language detection confidence
- score += confidence * 2.0
-
- # Factor 2: Code length (not too short, not too long)
- code_length = len(code.strip())
- if 20 <= code_length <= 500:
- score += 1.0
- elif 500 < code_length <= 2000:
- score += 0.5
- elif code_length < 10:
- score -= 2.0
-
- # Factor 3: Number of lines
- lines = [l for l in code.split('\n') if l.strip()]
- if 2 <= len(lines) <= 50:
- score += 1.0
- elif len(lines) > 100:
- score -= 1.0
-
- # Factor 4: Has function/class definitions
- if re.search(r'\b(def|function|class|func|fn|public class)\b', code):
- score += 1.5
-
- # Factor 5: Has meaningful variable names (not just x, y, i)
- meaningful_vars = re.findall(r'\b[a-z_][a-z0-9_]{3,}\b', code.lower())
- if len(meaningful_vars) >= 2:
- score += 1.0
-
- # Factor 6: Syntax validation
- is_valid, issues = self.validate_code_syntax(code, language)
- if is_valid:
- score += 1.0
- else:
- score -= len(issues) * 0.5
-
- # Clamp score to 0-10 range
- return max(0, min(10, score))
-
- def detect_code_blocks_by_font(self, page):
- """
- Detect code blocks by analyzing font properties.
- Monospace fonts typically indicate code.
-
- Returns list of detected code blocks with metadata.
- """
- code_blocks = []
- blocks = page.get_text("dict")["blocks"]
-
- monospace_fonts = ['courier', 'mono', 'consolas', 'menlo', 'monaco', 'dejavu']
-
- current_code = []
- current_font = None
-
- for block in blocks:
- if 'lines' not in block:
- continue
-
- for line in block['lines']:
- for span in line['spans']:
- font = span['font'].lower()
- text = span['text']
-
- # Check if font is monospace
- is_monospace = any(mf in font for mf in monospace_fonts)
-
- if is_monospace:
- # Accumulate code text
- current_code.append(text)
- current_font = span['font']
- else:
- # End of code block
- if current_code:
- code_text = ''.join(current_code).strip()
- if len(code_text) > 10: # Minimum code length
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'font': current_font,
- 'detection_method': 'font'
- })
- current_code = []
- current_font = None
-
- # Handle final code block
- if current_code:
- code_text = ''.join(current_code).strip()
- if len(code_text) > 10:
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'font': current_font,
- 'detection_method': 'font'
- })
-
- return code_blocks
-
- def detect_code_blocks_by_indent(self, text):
- """
- Detect code blocks by indentation patterns.
- Code often has consistent indentation.
-
- Returns list of detected code blocks.
- """
- code_blocks = []
- lines = text.split('\n')
- current_block = []
- indent_pattern = None
-
- for line in lines:
- # Check for indentation (4 spaces or tab)
- if line.startswith(' ') or line.startswith('\t'):
- # Start or continue code block
- if not indent_pattern:
- indent_pattern = line[:4] if line.startswith(' ') else '\t'
- current_block.append(line)
- else:
- # End of code block
- if current_block and len(current_block) >= 2: # At least 2 lines
- code_text = '\n'.join(current_block).strip()
- if len(code_text) > 20: # Minimum code length
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'detection_method': 'indent'
- })
- current_block = []
- indent_pattern = None
-
- # Handle final block
- if current_block and len(current_block) >= 2:
- code_text = '\n'.join(current_block).strip()
- if len(code_text) > 20:
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'detection_method': 'indent'
- })
-
- return code_blocks
-
- def detect_code_blocks_by_pattern(self, text):
- """
- Detect code blocks by common code patterns (keywords, syntax).
-
- Returns list of detected code snippets.
- """
- code_blocks = []
-
- # Common code patterns that span multiple lines
- patterns = [
- # Function definitions
- (r'((?:def|function|func|fn|public|private)\s+\w+\s*\([^)]*\)\s*[{:]?[^}]*[}]?)', 'function'),
- # Class definitions
- (r'(class\s+\w+[^{]*\{[^}]*\})', 'class'),
- # Import statements block
- (r'((?:import|require|use|include)[^\n]+(?:\n(?:import|require|use|include)[^\n]+)*)', 'imports'),
- ]
-
- for pattern, block_type in patterns:
- matches = re.finditer(pattern, text, re.MULTILINE | re.DOTALL)
- for match in matches:
- code_text = match.group(1).strip()
- if len(code_text) > 15:
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'detection_method': 'pattern',
- 'pattern_type': block_type
- })
-
- return code_blocks
-
- def detect_chapter_start(self, page_data):
- """
- Detect if a page starts a new chapter/section.
-
- Returns (is_chapter_start, chapter_title) tuple.
- """
- headings = page_data.get('headings', [])
-
- # Check for h1 or h2 at start of page
- if headings:
- first_heading = headings[0]
- # H1 headings are strong indicators of chapters
- if first_heading['level'] in ['h1', 'h2']:
- return True, first_heading['text']
-
- # Check for specific chapter markers in text
- text = page_data.get('text', '')
- first_line = text.split('\n')[0] if text else ''
-
- chapter_patterns = [
- r'^Chapter\s+\d+',
- r'^Part\s+\d+',
- r'^Section\s+\d+',
- r'^\d+\.\s+[A-Z]', # "1. Introduction"
- ]
-
- for pattern in chapter_patterns:
- if re.match(pattern, first_line, re.IGNORECASE):
- return True, first_line.strip()
-
- return False, None
-
- def merge_continued_code_blocks(self, pages):
- """
- Merge code blocks that are split across pages.
-
- Detects when a code block at the end of one page continues
- on the next page.
- """
- for i in range(len(pages) - 1):
- current_page = pages[i]
- next_page = pages[i + 1]
-
- # Check if current page has code blocks
- if not current_page['code_samples']:
- continue
-
- # Get last code block of current page
- last_code = current_page['code_samples'][-1]
-
- # Check if next page starts with code
- if not next_page['code_samples']:
- continue
-
- first_next_code = next_page['code_samples'][0]
-
- # Same language and detection method = likely continuation
- if (last_code['language'] == first_next_code['language'] and
- last_code['detection_method'] == first_next_code['detection_method']):
-
- # Check if last code block looks incomplete (doesn't end with closing brace/etc)
- last_code_text = last_code['code'].rstrip()
- continuation_indicators = [
- not last_code_text.endswith('}'),
- not last_code_text.endswith(';'),
- last_code_text.endswith(','),
- last_code_text.endswith('\\'),
- ]
-
- if any(continuation_indicators):
- # Merge the code blocks
- merged_code = last_code['code'] + '\n' + first_next_code['code']
- last_code['code'] = merged_code
- last_code['merged_from_next_page'] = True
-
- # Remove the first code block from next page
- next_page['code_samples'].pop(0)
- next_page['code_blocks_count'] -= 1
-
- self.log(f" Merged code block from page {i+1} to {i+2}")
-
- return pages
-
- def create_chunks(self, pages):
- """
- Create chunks of pages for better organization.
-
- Returns array of chunks, each containing:
- - chunk_number
- - start_page, end_page
- - pages (array)
- - chapter_title (if detected)
- """
- if self.chunk_size == 0:
- # No chunking - return all pages as one chunk
- return [{
- 'chunk_number': 1,
- 'start_page': 1,
- 'end_page': len(pages),
- 'pages': pages,
- 'chapter_title': None
- }]
-
- chunks = []
- current_chunk = []
- chunk_start = 0
- current_chapter = None
-
- for i, page in enumerate(pages):
- # Check if this page starts a new chapter
- is_chapter, chapter_title = self.detect_chapter_start(page)
-
- if is_chapter and current_chunk:
- # Save current chunk before starting new one
- chunks.append({
- 'chunk_number': len(chunks) + 1,
- 'start_page': chunk_start + 1,
- 'end_page': i,
- 'pages': current_chunk,
- 'chapter_title': current_chapter
- })
- current_chunk = []
- chunk_start = i
- current_chapter = chapter_title
-
- if not current_chapter and is_chapter:
- current_chapter = chapter_title
-
- current_chunk.append(page)
-
- # Check if chunk size reached (but don't break chapters)
- if not is_chapter and len(current_chunk) >= self.chunk_size:
- chunks.append({
- 'chunk_number': len(chunks) + 1,
- 'start_page': chunk_start + 1,
- 'end_page': i + 1,
- 'pages': current_chunk,
- 'chapter_title': current_chapter
- })
- current_chunk = []
- chunk_start = i + 1
- current_chapter = None
-
- # Add remaining pages as final chunk
- if current_chunk:
- chunks.append({
- 'chunk_number': len(chunks) + 1,
- 'start_page': chunk_start + 1,
- 'end_page': len(pages),
- 'pages': current_chunk,
- 'chapter_title': current_chapter
- })
-
- return chunks
-
- def extract_images_from_page(self, page, page_num):
- """
- Extract images from a PDF page and save to disk (NEW in B1.5).
-
- Returns list of extracted image metadata.
- """
- if not self.extract_images:
- # Just count images, don't extract
- return []
-
- extracted = []
- image_list = page.get_images()
-
- for img_index, img in enumerate(image_list):
- try:
- xref = img[0] # Image XREF number
- base_image = self.doc.extract_image(xref)
-
- if not base_image:
- continue
-
- image_bytes = base_image["image"]
- image_ext = base_image["ext"] # png, jpeg, etc.
- width = base_image.get("width", 0)
- height = base_image.get("height", 0)
-
- # Filter out small images (icons, bullets, etc.)
- if width < self.min_image_size or height < self.min_image_size:
- self.log(f" Skipping small image: {width}x{height}")
- continue
-
- # Generate filename
- pdf_basename = Path(self.pdf_path).stem
- image_filename = f"{pdf_basename}_page{page_num+1}_img{img_index+1}.{image_ext}"
-
- # Save image
- image_path = Path(self.image_dir) / image_filename
- image_path.parent.mkdir(parents=True, exist_ok=True)
-
- with open(image_path, "wb") as f:
- f.write(image_bytes)
-
- # Store metadata
- image_info = {
- 'filename': image_filename,
- 'path': str(image_path),
- 'page_number': page_num + 1,
- 'width': width,
- 'height': height,
- 'format': image_ext,
- 'size_bytes': len(image_bytes),
- 'xref': xref
- }
-
- extracted.append(image_info)
- self.extracted_images.append(image_info)
- self.log(f" Extracted image: {image_filename} ({width}x{height})")
-
- except Exception as e:
- self.log(f" Error extracting image {img_index}: {e}")
- continue
-
- return extracted
-
- def extract_page(self, page_num):
- """
- Extract content from a single PDF page.
-
- Returns dict with page content, code blocks, and metadata.
- """
- # Check cache first (Priority 3)
- cache_key = f"page_{page_num}"
- cached = self.get_cached(cache_key)
- if cached is not None:
- self.log(f" Page {page_num + 1}: Using cached data")
- return cached
-
- page = self.doc.load_page(page_num)
-
- # Extract plain text (with OCR if enabled - Priority 2)
- if self.use_ocr:
- text = self.extract_text_with_ocr(page)
- else:
- text = page.get_text("text")
-
- # Extract markdown (better structure preservation)
- markdown = page.get_text("markdown")
-
- # Extract tables (Priority 2)
- tables = self.extract_tables_from_page(page)
-
- # Get page images (for diagrams)
- images = page.get_images()
-
- # Extract images to files (NEW in B1.5)
- extracted_images = self.extract_images_from_page(page, page_num)
-
- # Detect code blocks using multiple methods
- font_code_blocks = self.detect_code_blocks_by_font(page)
- indent_code_blocks = self.detect_code_blocks_by_indent(text)
- pattern_code_blocks = self.detect_code_blocks_by_pattern(text)
-
- # Merge and deduplicate code blocks
- all_code_blocks = font_code_blocks + indent_code_blocks + pattern_code_blocks
-
- # Simple deduplication by code content
- unique_code = {}
- for block in all_code_blocks:
- code_hash = hash(block['code'])
- if code_hash not in unique_code:
- unique_code[code_hash] = block
- else:
- # Keep the one with higher quality score
- if block['quality_score'] > unique_code[code_hash]['quality_score']:
- unique_code[code_hash] = block
-
- code_samples = list(unique_code.values())
-
- # Filter by minimum quality (NEW in B1.4)
- if self.min_quality > 0:
- code_samples_before = len(code_samples)
- code_samples = [c for c in code_samples if c['quality_score'] >= self.min_quality]
- filtered_count = code_samples_before - len(code_samples)
- if filtered_count > 0:
- self.log(f" Filtered out {filtered_count} low-quality code blocks (min_quality={self.min_quality})")
-
- # Sort by quality score (highest first)
- code_samples.sort(key=lambda x: x['quality_score'], reverse=True)
-
- # Extract headings from markdown
- headings = []
- for line in markdown.split('\n'):
- if line.startswith('#'):
- level = len(line) - len(line.lstrip('#'))
- text = line.lstrip('#').strip()
- if text:
- headings.append({
- 'level': f'h{level}',
- 'text': text
- })
-
- page_data = {
- 'page_number': page_num + 1, # 1-indexed for humans
- 'text': text.strip(),
- 'markdown': markdown.strip(),
- 'headings': headings,
- 'code_samples': code_samples,
- 'images_count': len(images),
- 'extracted_images': extracted_images, # NEW in B1.5
- 'tables': tables, # NEW in Priority 2
- 'char_count': len(text),
- 'code_blocks_count': len(code_samples),
- 'tables_count': len(tables) # NEW in Priority 2
- }
-
- # Cache the result (Priority 3)
- self.set_cached(cache_key, page_data)
-
- self.log(f" Page {page_num + 1}: {len(text)} chars, {len(code_samples)} code blocks, {len(headings)} headings, {len(extracted_images)} images, {len(tables)} tables")
-
- return page_data
-
- def extract_all(self):
- """
- Extract content from all pages of the PDF.
- Enhanced with password support and parallel processing.
-
- Returns dict with metadata and pages array.
- """
- print(f"\n📄 Extracting from: {self.pdf_path}")
-
- # Open PDF (with password support - Priority 2)
- try:
- self.doc = fitz.open(self.pdf_path)
-
- # Handle encrypted PDFs (Priority 2)
- if self.doc.is_encrypted:
- if self.password:
- print(f" 🔐 PDF is encrypted, trying password...")
- if self.doc.authenticate(self.password):
- print(f" ✅ Password accepted")
- else:
- print(f" ❌ Invalid password")
- return None
- else:
- print(f" ❌ PDF is encrypted but no password provided")
- print(f" Use --password option to provide password")
- return None
-
- except Exception as e:
- print(f"❌ Error opening PDF: {e}")
- return None
-
- print(f" Pages: {len(self.doc)}")
- print(f" Metadata: {self.doc.metadata}")
-
- # Set up image directory (NEW in B1.5)
- if self.extract_images and not self.image_dir:
- pdf_basename = Path(self.pdf_path).stem
- self.image_dir = f"output/{pdf_basename}_images"
- print(f" Image directory: {self.image_dir}")
-
- # Show feature status
- if self.use_ocr:
- status = "✅ enabled" if TESSERACT_AVAILABLE else "⚠️ not available (install pytesseract)"
- print(f" OCR: {status}")
- if self.extract_tables:
- print(f" Table extraction: ✅ enabled")
- if self.parallel:
- status = "✅ enabled" if CONCURRENT_AVAILABLE else "⚠️ not available"
- print(f" Parallel processing: {status} ({self.max_workers} workers)")
- if self.use_cache:
- print(f" Caching: ✅ enabled")
-
- print("")
-
- # Extract each page (with parallel processing - Priority 3)
- if self.parallel and CONCURRENT_AVAILABLE and len(self.doc) > 5:
- print(f"🚀 Extracting {len(self.doc)} pages in parallel ({self.max_workers} workers)...")
- with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
- page_numbers = list(range(len(self.doc)))
- self.pages = list(executor.map(self.extract_page, page_numbers))
- else:
- # Sequential extraction
- for page_num in range(len(self.doc)):
- page_data = self.extract_page(page_num)
- self.pages.append(page_data)
-
- # Merge code blocks that span across pages
- self.log("\n🔗 Merging code blocks across pages...")
- self.pages = self.merge_continued_code_blocks(self.pages)
-
- # Create chunks
- self.log(f"\n📦 Creating chunks (chunk_size={self.chunk_size})...")
- chunks = self.create_chunks(self.pages)
-
- # Build summary
- total_chars = sum(p['char_count'] for p in self.pages)
- total_code_blocks = sum(p['code_blocks_count'] for p in self.pages)
- total_headings = sum(len(p['headings']) for p in self.pages)
- total_images = sum(p['images_count'] for p in self.pages)
- total_tables = sum(p['tables_count'] for p in self.pages) # NEW in Priority 2
-
- # Detect languages used
- languages = {}
- all_code_blocks_list = []
- for page in self.pages:
- for code in page['code_samples']:
- lang = code['language']
- languages[lang] = languages.get(lang, 0) + 1
- all_code_blocks_list.append(code)
-
- # Calculate quality statistics (NEW in B1.4)
- quality_stats = {}
- if all_code_blocks_list:
- quality_scores = [c['quality_score'] for c in all_code_blocks_list]
- confidences = [c['confidence'] for c in all_code_blocks_list]
- valid_count = sum(1 for c in all_code_blocks_list if c['is_valid'])
-
- quality_stats = {
- 'average_quality': sum(quality_scores) / len(quality_scores),
- 'average_confidence': sum(confidences) / len(confidences),
- 'valid_code_blocks': valid_count,
- 'invalid_code_blocks': total_code_blocks - valid_count,
- 'validation_rate': valid_count / total_code_blocks if total_code_blocks > 0 else 0,
- 'high_quality_blocks': sum(1 for s in quality_scores if s >= 7.0),
- 'medium_quality_blocks': sum(1 for s in quality_scores if 4.0 <= s < 7.0),
- 'low_quality_blocks': sum(1 for s in quality_scores if s < 4.0),
- }
-
- # Extract chapter information
- chapters = []
- for chunk in chunks:
- if chunk['chapter_title']:
- chapters.append({
- 'title': chunk['chapter_title'],
- 'start_page': chunk['start_page'],
- 'end_page': chunk['end_page']
- })
-
- result = {
- 'source_file': self.pdf_path,
- 'metadata': self.doc.metadata,
- 'total_pages': len(self.doc),
- 'total_chars': total_chars,
- 'total_code_blocks': total_code_blocks,
- 'total_headings': total_headings,
- 'total_images': total_images,
- 'total_extracted_images': len(self.extracted_images), # NEW in B1.5
- 'total_tables': total_tables, # NEW in Priority 2
- 'image_directory': self.image_dir if self.extract_images else None, # NEW in B1.5
- 'extracted_images': self.extracted_images, # NEW in B1.5
- 'total_chunks': len(chunks),
- 'chapters': chapters,
- 'languages_detected': languages,
- 'quality_statistics': quality_stats, # NEW in B1.4
- 'chunks': chunks,
- 'pages': self.pages # Still include all pages for compatibility
- }
-
- # Close document
- self.doc.close()
-
- print(f"\n✅ Extraction complete:")
- print(f" Total characters: {total_chars:,}")
- print(f" Code blocks found: {total_code_blocks}")
- print(f" Headings found: {total_headings}")
- print(f" Images found: {total_images}")
- if self.extract_images:
- print(f" Images extracted: {len(self.extracted_images)}")
- if self.image_dir:
- print(f" Image directory: {self.image_dir}")
- if self.extract_tables:
- print(f" Tables found: {total_tables}")
- print(f" Chunks created: {len(chunks)}")
- print(f" Chapters detected: {len(chapters)}")
- print(f" Languages detected: {', '.join(languages.keys())}")
-
- # Print quality statistics (NEW in B1.4)
- if quality_stats:
- print(f"\n📊 Code Quality Statistics:")
- print(f" Average quality: {quality_stats['average_quality']:.1f}/10")
- print(f" Average confidence: {quality_stats['average_confidence']:.1%}")
- print(f" Valid code blocks: {quality_stats['valid_code_blocks']}/{total_code_blocks} ({quality_stats['validation_rate']:.1%})")
- print(f" High quality (7+): {quality_stats['high_quality_blocks']}")
- print(f" Medium quality (4-7): {quality_stats['medium_quality_blocks']}")
- print(f" Low quality (<4): {quality_stats['low_quality_blocks']}")
-
- return result
-
-
-def main():
- parser = argparse.ArgumentParser(
- description='Extract text and code blocks from PDF documentation',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Extract from PDF
- python3 pdf_extractor_poc.py input.pdf
-
- # Save to JSON file
- python3 pdf_extractor_poc.py input.pdf --output result.json
-
- # Verbose mode
- python3 pdf_extractor_poc.py input.pdf --verbose
-
- # Extract and save
- python3 pdf_extractor_poc.py docs/python.pdf -o python_extracted.json -v
- """
- )
-
- parser.add_argument('pdf_file', help='Path to PDF file to extract')
- parser.add_argument('-o', '--output', help='Output JSON file path (default: print to stdout)')
- parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
- parser.add_argument('--pretty', action='store_true', help='Pretty-print JSON output')
- parser.add_argument('--chunk-size', type=int, default=10,
- help='Pages per chunk (0 = no chunking, default: 10)')
- parser.add_argument('--no-merge', action='store_true',
- help='Disable merging code blocks across pages')
- parser.add_argument('--min-quality', type=float, default=0.0,
- help='Minimum code quality score (0-10, default: 0 = no filtering)')
- parser.add_argument('--extract-images', action='store_true',
- help='Extract images to files (NEW in B1.5)')
- parser.add_argument('--image-dir', type=str, default=None,
- help='Directory to save extracted images (default: output/{pdf_name}_images)')
- parser.add_argument('--min-image-size', type=int, default=100,
- help='Minimum image dimension in pixels (filters icons, default: 100)')
-
- # Advanced features (Priority 2 & 3)
- parser.add_argument('--ocr', action='store_true',
- help='Use OCR for scanned PDFs (requires pytesseract)')
- parser.add_argument('--password', type=str, default=None,
- help='Password for encrypted PDF')
- parser.add_argument('--extract-tables', action='store_true',
- help='Extract tables from PDF (Priority 2)')
- parser.add_argument('--parallel', action='store_true',
- help='Process pages in parallel (Priority 3)')
- parser.add_argument('--workers', type=int, default=None,
- help='Number of parallel workers (default: CPU count)')
- parser.add_argument('--no-cache', action='store_true',
- help='Disable caching of expensive operations')
-
- args = parser.parse_args()
-
- # Validate input file
- if not os.path.exists(args.pdf_file):
- print(f"❌ Error: File not found: {args.pdf_file}")
- sys.exit(1)
-
- if not args.pdf_file.lower().endswith('.pdf'):
- print(f"⚠️ Warning: File does not have .pdf extension")
-
- # Extract
- extractor = PDFExtractor(
- args.pdf_file,
- verbose=args.verbose,
- chunk_size=args.chunk_size,
- min_quality=args.min_quality,
- extract_images=args.extract_images,
- image_dir=args.image_dir,
- min_image_size=args.min_image_size,
- # Advanced features (Priority 2 & 3)
- use_ocr=args.ocr,
- password=args.password,
- extract_tables=args.extract_tables,
- parallel=args.parallel,
- max_workers=args.workers,
- use_cache=not args.no_cache
- )
- result = extractor.extract_all()
-
- if result is None:
- sys.exit(1)
-
- # Output
- if args.output:
- # Save to file
- with open(args.output, 'w', encoding='utf-8') as f:
- if args.pretty:
- json.dump(result, f, indent=2, ensure_ascii=False)
- else:
- json.dump(result, f, ensure_ascii=False)
- print(f"\n💾 Saved to: {args.output}")
- else:
- # Print to stdout
- if args.pretty:
- print("\n" + json.dumps(result, indent=2, ensure_ascii=False))
- else:
- print(json.dumps(result, ensure_ascii=False))
-
-
-if __name__ == '__main__':
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/pdf_scraper.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/pdf_scraper.py
deleted file mode 100644
index 76ce377..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/pdf_scraper.py
+++ /dev/null
@@ -1,401 +0,0 @@
-#!/usr/bin/env python3
-"""
-PDF Documentation to Claude Skill Converter (Task B1.6)
-
-Converts PDF documentation into Claude AI skills.
-Uses pdf_extractor_poc.py for extraction, builds skill structure.
-
-Usage:
- python3 pdf_scraper.py --config configs/manual_pdf.json
- python3 pdf_scraper.py --pdf manual.pdf --name myskill
- python3 pdf_scraper.py --from-json manual_extracted.json
-"""
-
-import os
-import sys
-import json
-import re
-import argparse
-from pathlib import Path
-
-# Import the PDF extractor
-from .pdf_extractor_poc import PDFExtractor
-
-
-class PDFToSkillConverter:
- """Convert PDF documentation to Claude skill"""
-
- def __init__(self, config):
- self.config = config
- self.name = config['name']
- self.pdf_path = config.get('pdf_path', '')
- self.description = config.get('description', f'Documentation skill for {self.name}')
-
- # Paths
- self.skill_dir = f"output/{self.name}"
- self.data_file = f"output/{self.name}_extracted.json"
-
- # Extraction options
- self.extract_options = config.get('extract_options', {})
-
- # Categories
- self.categories = config.get('categories', {})
-
- # Extracted data
- self.extracted_data = None
-
- def extract_pdf(self):
- """Extract content from PDF using pdf_extractor_poc.py"""
- print(f"\n🔍 Extracting from PDF: {self.pdf_path}")
-
- # Create extractor with options
- extractor = PDFExtractor(
- self.pdf_path,
- verbose=True,
- chunk_size=self.extract_options.get('chunk_size', 10),
- min_quality=self.extract_options.get('min_quality', 5.0),
- extract_images=self.extract_options.get('extract_images', True),
- image_dir=f"{self.skill_dir}/assets/images",
- min_image_size=self.extract_options.get('min_image_size', 100)
- )
-
- # Extract
- result = extractor.extract_all()
-
- if not result:
- print("❌ Extraction failed")
- raise RuntimeError(f"Failed to extract PDF: {self.pdf_path}")
-
- # Save extracted data
- with open(self.data_file, 'w', encoding='utf-8') as f:
- json.dump(result, f, indent=2, ensure_ascii=False)
-
- print(f"\n💾 Saved extracted data to: {self.data_file}")
- self.extracted_data = result
- return True
-
- def load_extracted_data(self, json_path):
- """Load previously extracted data from JSON"""
- print(f"\n📂 Loading extracted data from: {json_path}")
-
- with open(json_path, 'r', encoding='utf-8') as f:
- self.extracted_data = json.load(f)
-
- print(f"✅ Loaded {self.extracted_data['total_pages']} pages")
- return True
-
- def categorize_content(self):
- """Categorize pages based on chapters or keywords"""
- print(f"\n📋 Categorizing content...")
-
- categorized = {}
-
- # Use chapters if available
- if self.extracted_data.get('chapters'):
- for chapter in self.extracted_data['chapters']:
- category_key = self._sanitize_filename(chapter['title'])
- categorized[category_key] = {
- 'title': chapter['title'],
- 'pages': []
- }
-
- # Assign pages to chapters
- for page in self.extracted_data['pages']:
- page_num = page['page_number']
-
- # Find which chapter this page belongs to
- for chapter in self.extracted_data['chapters']:
- if chapter['start_page'] <= page_num <= chapter['end_page']:
- category_key = self._sanitize_filename(chapter['title'])
- categorized[category_key]['pages'].append(page)
- break
-
- # Fall back to keyword-based categorization
- elif self.categories:
- # Check if categories is already in the right format (for tests)
- # If first value is a list of dicts (pages), use as-is
- first_value = next(iter(self.categories.values()))
- if isinstance(first_value, list) and first_value and isinstance(first_value[0], dict):
- # Already categorized - convert to expected format
- for cat_key, pages in self.categories.items():
- categorized[cat_key] = {
- 'title': cat_key.replace('_', ' ').title(),
- 'pages': pages
- }
- else:
- # Keyword-based categorization
- # Initialize categories
- for cat_key, keywords in self.categories.items():
- categorized[cat_key] = {
- 'title': cat_key.replace('_', ' ').title(),
- 'pages': []
- }
-
- # Categorize by keywords
- for page in self.extracted_data['pages']:
- text = page.get('text', '').lower()
- headings_text = ' '.join([h['text'] for h in page.get('headings', [])]).lower()
-
- # Score against each category
- scores = {}
- for cat_key, keywords in self.categories.items():
- # Handle both string keywords and dict keywords (shouldn't happen, but be safe)
- if isinstance(keywords, list):
- score = sum(1 for kw in keywords
- if isinstance(kw, str) and (kw.lower() in text or kw.lower() in headings_text))
- else:
- score = 0
- if score > 0:
- scores[cat_key] = score
-
- # Assign to highest scoring category
- if scores:
- best_cat = max(scores, key=scores.get)
- categorized[best_cat]['pages'].append(page)
- else:
- # Default category
- if 'other' not in categorized:
- categorized['other'] = {'title': 'Other', 'pages': []}
- categorized['other']['pages'].append(page)
-
- else:
- # No categorization - use single category
- categorized['content'] = {
- 'title': 'Content',
- 'pages': self.extracted_data['pages']
- }
-
- print(f"✅ Created {len(categorized)} categories")
- for cat_key, cat_data in categorized.items():
- print(f" - {cat_data['title']}: {len(cat_data['pages'])} pages")
-
- return categorized
-
- def build_skill(self):
- """Build complete skill structure"""
- print(f"\n🏗️ Building skill: {self.name}")
-
- # Create directories
- os.makedirs(f"{self.skill_dir}/references", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/scripts", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/assets", exist_ok=True)
-
- # Categorize content
- categorized = self.categorize_content()
-
- # Generate reference files
- print(f"\n📝 Generating reference files...")
- for cat_key, cat_data in categorized.items():
- self._generate_reference_file(cat_key, cat_data)
-
- # Generate index
- self._generate_index(categorized)
-
- # Generate SKILL.md
- self._generate_skill_md(categorized)
-
- print(f"\n✅ Skill built successfully: {self.skill_dir}/")
- print(f"\n📦 Next step: Package with: skill-seekers package {self.skill_dir}/")
-
- def _generate_reference_file(self, cat_key, cat_data):
- """Generate a reference markdown file for a category"""
- filename = f"{self.skill_dir}/references/{cat_key}.md"
-
- with open(filename, 'w', encoding='utf-8') as f:
- f.write(f"# {cat_data['title']}\n\n")
-
- for page in cat_data['pages']:
- # Add headings as section markers
- if page.get('headings'):
- f.write(f"## {page['headings'][0]['text']}\n\n")
-
- # Add text content
- if page.get('text'):
- # Limit to first 1000 chars per page to avoid huge files
- text = page['text'][:1000]
- f.write(f"{text}\n\n")
-
- # Add code samples (check both 'code_samples' and 'code_blocks' for compatibility)
- code_list = page.get('code_samples') or page.get('code_blocks')
- if code_list:
- f.write("### Code Examples\n\n")
- for code in code_list[:3]: # Limit to top 3
- lang = code.get('language', '')
- f.write(f"```{lang}\n{code['code']}\n```\n\n")
-
- # Add images
- if page.get('images'):
- # Create assets directory if needed
- assets_dir = os.path.join(self.skill_dir, 'assets')
- os.makedirs(assets_dir, exist_ok=True)
-
- f.write("### Images\n\n")
- for img in page['images']:
- # Save image to assets
- img_filename = f"page_{page['page_number']}_img_{img['index']}.png"
- img_path = os.path.join(assets_dir, img_filename)
-
- with open(img_path, 'wb') as img_file:
- img_file.write(img['data'])
-
- # Add markdown image reference
- f.write(f"![Image {img['index']}](../assets/{img_filename})\n\n")
-
- f.write("---\n\n")
-
- print(f" Generated: {filename}")
-
- def _generate_index(self, categorized):
- """Generate reference index"""
- filename = f"{self.skill_dir}/references/index.md"
-
- with open(filename, 'w', encoding='utf-8') as f:
- f.write(f"# {self.name.title()} Documentation Reference\n\n")
- f.write("## Categories\n\n")
-
- for cat_key, cat_data in categorized.items():
- page_count = len(cat_data['pages'])
- f.write(f"- [{cat_data['title']}]({cat_key}.md) ({page_count} pages)\n")
-
- f.write("\n## Statistics\n\n")
- stats = self.extracted_data.get('quality_statistics', {})
- f.write(f"- Total pages: {self.extracted_data.get('total_pages', 0)}\n")
- f.write(f"- Code blocks: {self.extracted_data.get('total_code_blocks', 0)}\n")
- f.write(f"- Images: {self.extracted_data.get('total_images', 0)}\n")
- if stats:
- f.write(f"- Average code quality: {stats.get('average_quality', 0):.1f}/10\n")
- f.write(f"- Valid code blocks: {stats.get('valid_code_blocks', 0)}\n")
-
- print(f" Generated: {filename}")
-
- def _generate_skill_md(self, categorized):
- """Generate main SKILL.md file"""
- filename = f"{self.skill_dir}/SKILL.md"
-
- # Generate skill name (lowercase, hyphens only, max 64 chars)
- skill_name = self.name.lower().replace('_', '-').replace(' ', '-')[:64]
-
- # Truncate description to 1024 chars if needed
- desc = self.description[:1024] if len(self.description) > 1024 else self.description
-
- with open(filename, 'w', encoding='utf-8') as f:
- # Write YAML frontmatter
- f.write(f"---\n")
- f.write(f"name: {skill_name}\n")
- f.write(f"description: {desc}\n")
- f.write(f"---\n\n")
-
- f.write(f"# {self.name.title()} Documentation Skill\n\n")
- f.write(f"{self.description}\n\n")
-
- f.write("## When to use this skill\n\n")
- f.write(f"Use this skill when the user asks about {self.name} documentation, ")
- f.write("including API references, tutorials, examples, and best practices.\n\n")
-
- f.write("## What's included\n\n")
- f.write("This skill contains:\n\n")
- for cat_key, cat_data in categorized.items():
- f.write(f"- **{cat_data['title']}**: {len(cat_data['pages'])} pages\n")
-
- f.write("\n## Quick Reference\n\n")
-
- # Get high-quality code samples
- all_code = []
- for page in self.extracted_data['pages']:
- all_code.extend(page.get('code_samples', []))
-
- # Sort by quality and get top 5
- all_code.sort(key=lambda x: x.get('quality_score', 0), reverse=True)
- top_code = all_code[:5]
-
- if top_code:
- f.write("### Top Code Examples\n\n")
- for i, code in enumerate(top_code, 1):
- lang = code['language']
- quality = code.get('quality_score', 0)
- f.write(f"**Example {i}** (Quality: {quality:.1f}/10):\n\n")
- f.write(f"```{lang}\n{code['code'][:300]}...\n```\n\n")
-
- f.write("## Navigation\n\n")
- f.write("See `references/index.md` for complete documentation structure.\n\n")
-
- # Add language statistics
- langs = self.extracted_data.get('languages_detected', {})
- if langs:
- f.write("## Languages Covered\n\n")
- for lang, count in sorted(langs.items(), key=lambda x: x[1], reverse=True):
- f.write(f"- {lang}: {count} examples\n")
-
- print(f" Generated: {filename}")
-
- def _sanitize_filename(self, name):
- """Convert string to safe filename"""
- # Remove special chars, replace spaces with underscores
- safe = re.sub(r'[^\w\s-]', '', name.lower())
- safe = re.sub(r'[-\s]+', '_', safe)
- return safe
-
-
-def main():
- parser = argparse.ArgumentParser(
- description='Convert PDF documentation to Claude skill',
- formatter_class=argparse.RawDescriptionHelpFormatter
- )
-
- parser.add_argument('--config', help='PDF config JSON file')
- parser.add_argument('--pdf', help='Direct PDF file path')
- parser.add_argument('--name', help='Skill name (with --pdf)')
- parser.add_argument('--from-json', help='Build skill from extracted JSON')
- parser.add_argument('--description', help='Skill description')
-
- args = parser.parse_args()
-
- # Validate inputs
- if not (args.config or args.pdf or args.from_json):
- parser.error("Must specify --config, --pdf, or --from-json")
-
- # Load or create config
- if args.config:
- with open(args.config, 'r') as f:
- config = json.load(f)
- elif args.from_json:
- # Build from extracted JSON
- name = Path(args.from_json).stem.replace('_extracted', '')
- config = {
- 'name': name,
- 'description': args.description or f'Documentation skill for {name}'
- }
- converter = PDFToSkillConverter(config)
- converter.load_extracted_data(args.from_json)
- converter.build_skill()
- return
- else:
- # Direct PDF mode
- if not args.name:
- parser.error("Must specify --name with --pdf")
- config = {
- 'name': args.name,
- 'pdf_path': args.pdf,
- 'description': args.description or f'Documentation skill for {args.name}',
- 'extract_options': {
- 'chunk_size': 10,
- 'min_quality': 5.0,
- 'extract_images': True,
- 'min_image_size': 100
- }
- }
-
- # Create converter
- converter = PDFToSkillConverter(config)
-
- # Extract if needed
- if config.get('pdf_path'):
- if not converter.extract_pdf():
- sys.exit(1)
-
- # Build skill
- converter.build_skill()
-
-
-if __name__ == '__main__':
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/quality_checker.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/quality_checker.py
deleted file mode 100644
index 8ff66c5..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/quality_checker.py
+++ /dev/null
@@ -1,480 +0,0 @@
-#!/usr/bin/env python3
-"""
-Quality Checker for Claude Skills
-Validates skill quality, checks links, and generates quality reports.
-
-Usage:
- python3 quality_checker.py output/react/
- python3 quality_checker.py output/godot/ --verbose
-"""
-
-import os
-import re
-import sys
-from pathlib import Path
-from typing import Dict, List, Tuple, Optional
-from dataclasses import dataclass, field
-
-
-@dataclass
-class QualityIssue:
- """Represents a quality issue found during validation."""
- level: str # 'error', 'warning', 'info'
- category: str # 'enhancement', 'content', 'links', 'structure'
- message: str
- file: Optional[str] = None
- line: Optional[int] = None
-
-
-@dataclass
-class QualityReport:
- """Complete quality report for a skill."""
- skill_name: str
- skill_path: Path
- errors: List[QualityIssue] = field(default_factory=list)
- warnings: List[QualityIssue] = field(default_factory=list)
- info: List[QualityIssue] = field(default_factory=list)
-
- def add_error(self, category: str, message: str, file: str = None, line: int = None):
- """Add an error to the report."""
- self.errors.append(QualityIssue('error', category, message, file, line))
-
- def add_warning(self, category: str, message: str, file: str = None, line: int = None):
- """Add a warning to the report."""
- self.warnings.append(QualityIssue('warning', category, message, file, line))
-
- def add_info(self, category: str, message: str, file: str = None, line: int = None):
- """Add info to the report."""
- self.info.append(QualityIssue('info', category, message, file, line))
-
- @property
- def has_errors(self) -> bool:
- """Check if there are any errors."""
- return len(self.errors) > 0
-
- @property
- def has_warnings(self) -> bool:
- """Check if there are any warnings."""
- return len(self.warnings) > 0
-
- @property
- def is_excellent(self) -> bool:
- """Check if quality is excellent (no errors, no warnings)."""
- return not self.has_errors and not self.has_warnings
-
- @property
- def quality_score(self) -> float:
- """Calculate quality score (0-100)."""
- # Start with perfect score
- score = 100.0
-
- # Deduct points for issues
- score -= len(self.errors) * 15 # -15 per error
- score -= len(self.warnings) * 5 # -5 per warning
-
- # Never go below 0
- return max(0.0, score)
-
- @property
- def quality_grade(self) -> str:
- """Get quality grade (A-F)."""
- score = self.quality_score
- if score >= 90:
- return 'A'
- elif score >= 80:
- return 'B'
- elif score >= 70:
- return 'C'
- elif score >= 60:
- return 'D'
- else:
- return 'F'
-
-
-class SkillQualityChecker:
- """Validates skill quality and generates reports."""
-
- def __init__(self, skill_dir: Path):
- """Initialize quality checker.
-
- Args:
- skill_dir: Path to skill directory
- """
- self.skill_dir = Path(skill_dir)
- self.skill_md_path = self.skill_dir / "SKILL.md"
- self.references_dir = self.skill_dir / "references"
- self.report = QualityReport(
- skill_name=self.skill_dir.name,
- skill_path=self.skill_dir
- )
-
- def check_all(self) -> QualityReport:
- """Run all quality checks and return report.
-
- Returns:
- QualityReport: Complete quality report
- """
- # Basic structure checks
- self._check_skill_structure()
-
- # Enhancement verification
- self._check_enhancement_quality()
-
- # Content quality checks
- self._check_content_quality()
-
- # Link validation
- self._check_links()
-
- return self.report
-
- def _check_skill_structure(self):
- """Check basic skill structure."""
- # Check SKILL.md exists
- if not self.skill_md_path.exists():
- self.report.add_error(
- 'structure',
- 'SKILL.md file not found',
- str(self.skill_md_path)
- )
- return
-
- # Check references directory exists
- if not self.references_dir.exists():
- self.report.add_warning(
- 'structure',
- 'references/ directory not found - skill may be incomplete',
- str(self.references_dir)
- )
- elif not list(self.references_dir.glob('*.md')):
- self.report.add_warning(
- 'structure',
- 'references/ directory is empty - no reference documentation found',
- str(self.references_dir)
- )
-
- def _check_enhancement_quality(self):
- """Check if SKILL.md was properly enhanced."""
- if not self.skill_md_path.exists():
- return
-
- content = self.skill_md_path.read_text(encoding='utf-8')
-
- # Check for template indicators (signs it wasn't enhanced)
- template_indicators = [
- "TODO:",
- "[Add description]",
- "[Framework specific tips]",
- "coming soon",
- ]
-
- for indicator in template_indicators:
- if indicator.lower() in content.lower():
- self.report.add_warning(
- 'enhancement',
- f'Found template placeholder: "{indicator}" - SKILL.md may not be enhanced',
- 'SKILL.md'
- )
-
- # Check for good signs of enhancement
- enhancement_indicators = {
- 'code_examples': re.compile(r'```[\w-]+\n', re.MULTILINE),
- 'real_examples': re.compile(r'Example:', re.IGNORECASE),
- 'sections': re.compile(r'^## .+', re.MULTILINE),
- }
-
- code_blocks = len(enhancement_indicators['code_examples'].findall(content))
- real_examples = len(enhancement_indicators['real_examples'].findall(content))
- sections = len(enhancement_indicators['sections'].findall(content))
-
- # Quality thresholds
- if code_blocks == 0:
- self.report.add_warning(
- 'enhancement',
- 'No code examples found in SKILL.md - consider enhancing',
- 'SKILL.md'
- )
- elif code_blocks < 3:
- self.report.add_info(
- 'enhancement',
- f'Only {code_blocks} code examples found - more examples would improve quality',
- 'SKILL.md'
- )
- else:
- self.report.add_info(
- 'enhancement',
- f'✓ Found {code_blocks} code examples',
- 'SKILL.md'
- )
-
- if sections < 4:
- self.report.add_warning(
- 'enhancement',
- f'Only {sections} sections found - SKILL.md may be too basic',
- 'SKILL.md'
- )
- else:
- self.report.add_info(
- 'enhancement',
- f'✓ Found {sections} sections',
- 'SKILL.md'
- )
-
- def _check_content_quality(self):
- """Check content quality."""
- if not self.skill_md_path.exists():
- return
-
- content = self.skill_md_path.read_text(encoding='utf-8')
-
- # Check YAML frontmatter
- if not content.startswith('---'):
- self.report.add_error(
- 'content',
- 'Missing YAML frontmatter - SKILL.md must start with ---',
- 'SKILL.md',
- 1
- )
- else:
- # Extract frontmatter
- try:
- frontmatter_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
- if frontmatter_match:
- frontmatter = frontmatter_match.group(1)
-
- # Check for required fields
- if 'name:' not in frontmatter:
- self.report.add_error(
- 'content',
- 'Missing "name:" field in YAML frontmatter',
- 'SKILL.md',
- 2
- )
-
- # Check for description
- if 'description:' in frontmatter:
- self.report.add_info(
- 'content',
- '✓ YAML frontmatter includes description',
- 'SKILL.md'
- )
- else:
- self.report.add_error(
- 'content',
- 'Invalid YAML frontmatter format',
- 'SKILL.md',
- 1
- )
- except Exception as e:
- self.report.add_error(
- 'content',
- f'Error parsing YAML frontmatter: {e}',
- 'SKILL.md',
- 1
- )
-
- # Check code block language tags
- code_blocks_without_lang = re.findall(r'```\n[^`]', content)
- if code_blocks_without_lang:
- self.report.add_warning(
- 'content',
- f'Found {len(code_blocks_without_lang)} code blocks without language tags',
- 'SKILL.md'
- )
-
- # Check for "When to Use" section
- if 'when to use' not in content.lower():
- self.report.add_warning(
- 'content',
- 'Missing "When to Use This Skill" section',
- 'SKILL.md'
- )
- else:
- self.report.add_info(
- 'content',
- '✓ Found "When to Use" section',
- 'SKILL.md'
- )
-
- # Check reference files
- if self.references_dir.exists():
- ref_files = list(self.references_dir.glob('*.md'))
- if ref_files:
- self.report.add_info(
- 'content',
- f'✓ Found {len(ref_files)} reference files',
- 'references/'
- )
-
- # Check if references are mentioned in SKILL.md
- mentioned_refs = 0
- for ref_file in ref_files:
- if ref_file.name in content:
- mentioned_refs += 1
-
- if mentioned_refs == 0:
- self.report.add_warning(
- 'content',
- 'Reference files exist but none are mentioned in SKILL.md',
- 'SKILL.md'
- )
-
- def _check_links(self):
- """Check internal markdown links."""
- if not self.skill_md_path.exists():
- return
-
- content = self.skill_md_path.read_text(encoding='utf-8')
-
- # Find all markdown links [text](path)
- link_pattern = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
- links = link_pattern.findall(content)
-
- broken_links = []
-
- for text, link in links:
- # Skip external links (http/https)
- if link.startswith('http://') or link.startswith('https://'):
- continue
-
- # Skip anchor links
- if link.startswith('#'):
- continue
-
- # Check if file exists (relative to SKILL.md)
- link_path = self.skill_dir / link
- if not link_path.exists():
- broken_links.append((text, link))
-
- if broken_links:
- for text, link in broken_links:
- self.report.add_warning(
- 'links',
- f'Broken link: [{text}]({link})',
- 'SKILL.md'
- )
- else:
- if links:
- internal_links = [l for t, l in links if not l.startswith('http')]
- if internal_links:
- self.report.add_info(
- 'links',
- f'✓ All {len(internal_links)} internal links are valid',
- 'SKILL.md'
- )
-
-
-def print_report(report: QualityReport, verbose: bool = False):
- """Print quality report to console.
-
- Args:
- report: Quality report to print
- verbose: Show all info messages
- """
- print("\n" + "=" * 60)
- print(f"QUALITY REPORT: {report.skill_name}")
- print("=" * 60)
- print()
-
- # Quality score
- print(f"Quality Score: {report.quality_score:.1f}/100 (Grade: {report.quality_grade})")
- print()
-
- # Errors
- if report.errors:
- print(f"❌ ERRORS ({len(report.errors)}):")
- for issue in report.errors:
- location = f" ({issue.file}:{issue.line})" if issue.file and issue.line else f" ({issue.file})" if issue.file else ""
- print(f" [{issue.category}] {issue.message}{location}")
- print()
-
- # Warnings
- if report.warnings:
- print(f"⚠️ WARNINGS ({len(report.warnings)}):")
- for issue in report.warnings:
- location = f" ({issue.file}:{issue.line})" if issue.file and issue.line else f" ({issue.file})" if issue.file else ""
- print(f" [{issue.category}] {issue.message}{location}")
- print()
-
- # Info (only in verbose mode)
- if verbose and report.info:
- print(f"ℹ️ INFO ({len(report.info)}):")
- for issue in report.info:
- location = f" ({issue.file})" if issue.file else ""
- print(f" [{issue.category}] {issue.message}{location}")
- print()
-
- # Summary
- if report.is_excellent:
- print("✅ EXCELLENT! No issues found.")
- elif not report.has_errors:
- print("✓ GOOD! No errors, but some warnings to review.")
- else:
- print("❌ NEEDS IMPROVEMENT! Please fix errors before packaging.")
-
- print()
-
-
-def main():
- """Main entry point."""
- import argparse
-
- parser = argparse.ArgumentParser(
- description="Check skill quality and generate report",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Basic quality check
- python3 quality_checker.py output/react/
-
- # Verbose mode (show all info)
- python3 quality_checker.py output/godot/ --verbose
-
- # Exit with error code if issues found
- python3 quality_checker.py output/django/ --strict
-"""
- )
-
- parser.add_argument(
- 'skill_directory',
- help='Path to skill directory (e.g., output/react/)'
- )
-
- parser.add_argument(
- '--verbose', '-v',
- action='store_true',
- help='Show all info messages'
- )
-
- parser.add_argument(
- '--strict',
- action='store_true',
- help='Exit with error code if any warnings or errors found'
- )
-
- args = parser.parse_args()
-
- # Check if directory exists
- skill_dir = Path(args.skill_directory)
- if not skill_dir.exists():
- print(f"❌ Directory not found: {skill_dir}")
- sys.exit(1)
-
- # Run quality checks
- checker = SkillQualityChecker(skill_dir)
- report = checker.check_all()
-
- # Print report
- print_report(report, verbose=args.verbose)
-
- # Exit code
- if args.strict and (report.has_errors or report.has_warnings):
- sys.exit(1)
- elif report.has_errors:
- sys.exit(1)
- else:
- sys.exit(0)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/run_tests.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/run_tests.py
deleted file mode 100644
index ab38fcc..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/run_tests.py
+++ /dev/null
@@ -1,228 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test Runner for Skill Seeker
-Runs all test suites and generates a comprehensive test report
-"""
-
-import sys
-import unittest
-import os
-from io import StringIO
-from pathlib import Path
-
-
-class ColoredTextTestResult(unittest.TextTestResult):
- """Custom test result class with colored output"""
-
- # ANSI color codes
- GREEN = '\033[92m'
- RED = '\033[91m'
- YELLOW = '\033[93m'
- BLUE = '\033[94m'
- RESET = '\033[0m'
- BOLD = '\033[1m'
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.test_results = []
-
- def addSuccess(self, test):
- super().addSuccess(test)
- self.test_results.append(('PASS', test))
- if self.showAll:
- self.stream.write(f"{self.GREEN}✓ PASS{self.RESET}\n")
- elif self.dots:
- self.stream.write(f"{self.GREEN}.{self.RESET}")
- self.stream.flush()
-
- def addError(self, test, err):
- super().addError(test, err)
- self.test_results.append(('ERROR', test))
- if self.showAll:
- self.stream.write(f"{self.RED}✗ ERROR{self.RESET}\n")
- elif self.dots:
- self.stream.write(f"{self.RED}E{self.RESET}")
- self.stream.flush()
-
- def addFailure(self, test, err):
- super().addFailure(test, err)
- self.test_results.append(('FAIL', test))
- if self.showAll:
- self.stream.write(f"{self.RED}✗ FAIL{self.RESET}\n")
- elif self.dots:
- self.stream.write(f"{self.RED}F{self.RESET}")
- self.stream.flush()
-
- def addSkip(self, test, reason):
- super().addSkip(test, reason)
- self.test_results.append(('SKIP', test))
- if self.showAll:
- self.stream.write(f"{self.YELLOW}⊘ SKIP{self.RESET}\n")
- elif self.dots:
- self.stream.write(f"{self.YELLOW}s{self.RESET}")
- self.stream.flush()
-
-
-class ColoredTextTestRunner(unittest.TextTestRunner):
- """Custom test runner with colored output"""
- resultclass = ColoredTextTestResult
-
-
-def discover_tests(test_dir='tests'):
- """Discover all test files in the tests directory"""
- loader = unittest.TestLoader()
- start_dir = test_dir
- pattern = 'test_*.py'
-
- suite = loader.discover(start_dir, pattern=pattern)
- return suite
-
-
-def run_specific_suite(suite_name):
- """Run a specific test suite"""
- loader = unittest.TestLoader()
-
- suite_map = {
- 'config': 'tests.test_config_validation',
- 'features': 'tests.test_scraper_features',
- 'integration': 'tests.test_integration'
- }
-
- if suite_name not in suite_map:
- print(f"Unknown test suite: {suite_name}")
- print(f"Available suites: {', '.join(suite_map.keys())}")
- return None
-
- module_name = suite_map[suite_name]
- try:
- suite = loader.loadTestsFromName(module_name)
- return suite
- except Exception as e:
- print(f"Error loading test suite '{suite_name}': {e}")
- return None
-
-
-def print_summary(result):
- """Print a detailed test summary"""
- total = result.testsRun
- passed = total - len(result.failures) - len(result.errors) - len(result.skipped)
- failed = len(result.failures)
- errors = len(result.errors)
- skipped = len(result.skipped)
-
- print("\n" + "="*70)
- print("TEST SUMMARY")
- print("="*70)
-
- # Overall stats
- print(f"\n{ColoredTextTestResult.BOLD}Total Tests:{ColoredTextTestResult.RESET} {total}")
- print(f"{ColoredTextTestResult.GREEN}✓ Passed:{ColoredTextTestResult.RESET} {passed}")
- if failed > 0:
- print(f"{ColoredTextTestResult.RED}✗ Failed:{ColoredTextTestResult.RESET} {failed}")
- if errors > 0:
- print(f"{ColoredTextTestResult.RED}✗ Errors:{ColoredTextTestResult.RESET} {errors}")
- if skipped > 0:
- print(f"{ColoredTextTestResult.YELLOW}⊘ Skipped:{ColoredTextTestResult.RESET} {skipped}")
-
- # Success rate
- if total > 0:
- success_rate = (passed / total) * 100
- color = ColoredTextTestResult.GREEN if success_rate == 100 else \
- ColoredTextTestResult.YELLOW if success_rate >= 80 else \
- ColoredTextTestResult.RED
- print(f"\n{color}Success Rate: {success_rate:.1f}%{ColoredTextTestResult.RESET}")
-
- # Category breakdown
- if hasattr(result, 'test_results'):
- print(f"\n{ColoredTextTestResult.BOLD}Test Breakdown by Category:{ColoredTextTestResult.RESET}")
-
- categories = {}
- for status, test in result.test_results:
- test_name = str(test)
- # Extract test class name
- if '.' in test_name:
- class_name = test_name.split('.')[0].split()[-1]
- if class_name not in categories:
- categories[class_name] = {'PASS': 0, 'FAIL': 0, 'ERROR': 0, 'SKIP': 0}
- categories[class_name][status] += 1
-
- for category, stats in sorted(categories.items()):
- total_cat = sum(stats.values())
- passed_cat = stats['PASS']
- print(f" {category}: {passed_cat}/{total_cat} passed")
-
- print("\n" + "="*70)
-
- # Return status
- return failed == 0 and errors == 0
-
-
-def main():
- """Main test runner"""
- import argparse
-
- parser = argparse.ArgumentParser(
- description='Run tests for Skill Seeker',
- formatter_class=argparse.RawDescriptionHelpFormatter
- )
-
- parser.add_argument('--suite', '-s', type=str,
- help='Run specific test suite (config, features, integration)')
- parser.add_argument('--verbose', '-v', action='store_true',
- help='Verbose output (show each test)')
- parser.add_argument('--quiet', '-q', action='store_true',
- help='Quiet output (minimal output)')
- parser.add_argument('--failfast', '-f', action='store_true',
- help='Stop on first failure')
- parser.add_argument('--list', '-l', action='store_true',
- help='List all available tests')
-
- args = parser.parse_args()
-
- # Set verbosity
- verbosity = 1
- if args.verbose:
- verbosity = 2
- elif args.quiet:
- verbosity = 0
-
- print(f"\n{ColoredTextTestResult.BOLD}{'='*70}{ColoredTextTestResult.RESET}")
- print(f"{ColoredTextTestResult.BOLD}SKILL SEEKER TEST SUITE{ColoredTextTestResult.RESET}")
- print(f"{ColoredTextTestResult.BOLD}{'='*70}{ColoredTextTestResult.RESET}\n")
-
- # Discover or load specific suite
- if args.suite:
- print(f"Running test suite: {ColoredTextTestResult.BLUE}{args.suite}{ColoredTextTestResult.RESET}\n")
- suite = run_specific_suite(args.suite)
- if suite is None:
- return 1
- else:
- print(f"Running {ColoredTextTestResult.BLUE}all tests{ColoredTextTestResult.RESET}\n")
- suite = discover_tests()
-
- # List tests
- if args.list:
- print("\nAvailable tests:\n")
- for test_group in suite:
- for test in test_group:
- print(f" - {test}")
- print()
- return 0
-
- # Run tests
- runner = ColoredTextTestRunner(
- verbosity=verbosity,
- failfast=args.failfast
- )
-
- result = runner.run(suite)
-
- # Print summary
- success = print_summary(result)
-
- # Return appropriate exit code
- return 0 if success else 1
-
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/split_config.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/split_config.py
deleted file mode 100644
index 40551ad..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/split_config.py
+++ /dev/null
@@ -1,320 +0,0 @@
-#!/usr/bin/env python3
-"""
-Config Splitter for Large Documentation Sites
-
-Splits large documentation configs into multiple smaller, focused skill configs.
-Supports multiple splitting strategies: category-based, size-based, and automatic.
-"""
-
-import json
-import sys
-import argparse
-from pathlib import Path
-from typing import Dict, List, Any, Tuple
-from collections import defaultdict
-
-
-class ConfigSplitter:
- """Splits large documentation configs into multiple focused configs"""
-
- def __init__(self, config_path: str, strategy: str = "auto", target_pages: int = 5000):
- self.config_path = Path(config_path)
- self.strategy = strategy
- self.target_pages = target_pages
- self.config = self.load_config()
- self.base_name = self.config['name']
-
- def load_config(self) -> Dict[str, Any]:
- """Load configuration from file"""
- try:
- with open(self.config_path, 'r') as f:
- return json.load(f)
- except FileNotFoundError:
- print(f"❌ Error: Config file not found: {self.config_path}")
- sys.exit(1)
- except json.JSONDecodeError as e:
- print(f"❌ Error: Invalid JSON in config file: {e}")
- sys.exit(1)
-
- def get_split_strategy(self) -> str:
- """Determine split strategy"""
- # Check if strategy is defined in config
- if 'split_strategy' in self.config:
- config_strategy = self.config['split_strategy']
- if config_strategy != "none":
- return config_strategy
-
- # Use provided strategy or auto-detect
- if self.strategy == "auto":
- max_pages = self.config.get('max_pages', 500)
-
- if max_pages < 5000:
- print(f"ℹ️ Small documentation ({max_pages} pages) - no splitting needed")
- return "none"
- elif max_pages < 10000 and 'categories' in self.config:
- print(f"ℹ️ Medium documentation ({max_pages} pages) - category split recommended")
- return "category"
- elif 'categories' in self.config and len(self.config['categories']) >= 3:
- print(f"ℹ️ Large documentation ({max_pages} pages) - router + categories recommended")
- return "router"
- else:
- print(f"ℹ️ Large documentation ({max_pages} pages) - size-based split")
- return "size"
-
- return self.strategy
-
- def split_by_category(self, create_router: bool = False) -> List[Dict[str, Any]]:
- """Split config by categories"""
- if 'categories' not in self.config:
- print("❌ Error: No categories defined in config")
- sys.exit(1)
-
- categories = self.config['categories']
- split_categories = self.config.get('split_config', {}).get('split_by_categories')
-
- # If specific categories specified, use only those
- if split_categories:
- categories = {k: v for k, v in categories.items() if k in split_categories}
-
- configs = []
-
- for category_name, keywords in categories.items():
- # Create new config for this category
- new_config = self.config.copy()
- new_config['name'] = f"{self.base_name}-{category_name}"
- new_config['description'] = f"{self.base_name.capitalize()} - {category_name.replace('_', ' ').title()}. {self.config.get('description', '')}"
-
- # Update URL patterns to focus on this category
- url_patterns = new_config.get('url_patterns', {})
-
- # Add category keywords to includes
- includes = url_patterns.get('include', [])
- for keyword in keywords:
- if keyword.startswith('/'):
- includes.append(keyword)
-
- if includes:
- url_patterns['include'] = list(set(includes))
- new_config['url_patterns'] = url_patterns
-
- # Keep only this category
- new_config['categories'] = {category_name: keywords}
-
- # Remove split config from child
- if 'split_strategy' in new_config:
- del new_config['split_strategy']
- if 'split_config' in new_config:
- del new_config['split_config']
-
- # Adjust max_pages estimate
- if 'max_pages' in new_config:
- new_config['max_pages'] = self.target_pages
-
- configs.append(new_config)
-
- print(f"✅ Created {len(configs)} category-based configs")
-
- # Optionally create router config
- if create_router:
- router_config = self.create_router_config(configs)
- configs.insert(0, router_config)
- print(f"✅ Created router config: {router_config['name']}")
-
- return configs
-
- def split_by_size(self) -> List[Dict[str, Any]]:
- """Split config by size (page count)"""
- max_pages = self.config.get('max_pages', 500)
- num_splits = (max_pages + self.target_pages - 1) // self.target_pages
-
- configs = []
-
- for i in range(num_splits):
- new_config = self.config.copy()
- part_num = i + 1
- new_config['name'] = f"{self.base_name}-part{part_num}"
- new_config['description'] = f"{self.base_name.capitalize()} - Part {part_num}. {self.config.get('description', '')}"
- new_config['max_pages'] = self.target_pages
-
- # Remove split config from child
- if 'split_strategy' in new_config:
- del new_config['split_strategy']
- if 'split_config' in new_config:
- del new_config['split_config']
-
- configs.append(new_config)
-
- print(f"✅ Created {len(configs)} size-based configs ({self.target_pages} pages each)")
- return configs
-
- def create_router_config(self, sub_configs: List[Dict[str, Any]]) -> Dict[str, Any]:
- """Create a router config that references sub-skills"""
- router_name = self.config.get('split_config', {}).get('router_name', self.base_name)
-
- router_config = {
- "name": router_name,
- "description": self.config.get('description', ''),
- "base_url": self.config['base_url'],
- "selectors": self.config['selectors'],
- "url_patterns": self.config.get('url_patterns', {}),
- "rate_limit": self.config.get('rate_limit', 0.5),
- "max_pages": 500, # Router only needs overview pages
- "_router": True,
- "_sub_skills": [cfg['name'] for cfg in sub_configs],
- "_routing_keywords": {
- cfg['name']: list(cfg.get('categories', {}).keys())
- for cfg in sub_configs
- }
- }
-
- return router_config
-
- def split(self) -> List[Dict[str, Any]]:
- """Execute split based on strategy"""
- strategy = self.get_split_strategy()
-
- print(f"\n{'='*60}")
- print(f"CONFIG SPLITTER: {self.base_name}")
- print(f"{'='*60}")
- print(f"Strategy: {strategy}")
- print(f"Target pages per skill: {self.target_pages}")
- print("")
-
- if strategy == "none":
- print("ℹ️ No splitting required")
- return [self.config]
-
- elif strategy == "category":
- return self.split_by_category(create_router=False)
-
- elif strategy == "router":
- create_router = self.config.get('split_config', {}).get('create_router', True)
- return self.split_by_category(create_router=create_router)
-
- elif strategy == "size":
- return self.split_by_size()
-
- else:
- print(f"❌ Error: Unknown strategy: {strategy}")
- sys.exit(1)
-
- def save_configs(self, configs: List[Dict[str, Any]], output_dir: Path = None) -> List[Path]:
- """Save configs to files"""
- if output_dir is None:
- output_dir = self.config_path.parent
-
- output_dir = Path(output_dir)
- output_dir.mkdir(parents=True, exist_ok=True)
-
- saved_files = []
-
- for config in configs:
- filename = f"{config['name']}.json"
- filepath = output_dir / filename
-
- with open(filepath, 'w') as f:
- json.dump(config, f, indent=2)
-
- saved_files.append(filepath)
- print(f" 💾 Saved: {filepath}")
-
- return saved_files
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Split large documentation configs into multiple focused skills",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Auto-detect strategy
- python3 split_config.py configs/godot.json
-
- # Use category-based split
- python3 split_config.py configs/godot.json --strategy category
-
- # Use router + categories
- python3 split_config.py configs/godot.json --strategy router
-
- # Custom target size
- python3 split_config.py configs/godot.json --target-pages 3000
-
- # Dry run (don't save files)
- python3 split_config.py configs/godot.json --dry-run
-
-Split Strategies:
- none - No splitting (single skill)
- auto - Automatically choose best strategy
- category - Split by categories defined in config
- router - Create router + category-based sub-skills
- size - Split by page count
- """
- )
-
- parser.add_argument(
- 'config',
- help='Path to config file (e.g., configs/godot.json)'
- )
-
- parser.add_argument(
- '--strategy',
- choices=['auto', 'none', 'category', 'router', 'size'],
- default='auto',
- help='Splitting strategy (default: auto)'
- )
-
- parser.add_argument(
- '--target-pages',
- type=int,
- default=5000,
- help='Target pages per skill (default: 5000)'
- )
-
- parser.add_argument(
- '--output-dir',
- help='Output directory for configs (default: same as input)'
- )
-
- parser.add_argument(
- '--dry-run',
- action='store_true',
- help='Show what would be created without saving files'
- )
-
- args = parser.parse_args()
-
- # Create splitter
- splitter = ConfigSplitter(args.config, args.strategy, args.target_pages)
-
- # Split config
- configs = splitter.split()
-
- if args.dry_run:
- print(f"\n{'='*60}")
- print("DRY RUN - No files saved")
- print(f"{'='*60}")
- print(f"Would create {len(configs)} config files:")
- for cfg in configs:
- is_router = cfg.get('_router', False)
- router_marker = " (ROUTER)" if is_router else ""
- print(f" 📄 {cfg['name']}.json{router_marker}")
- else:
- print(f"\n{'='*60}")
- print("SAVING CONFIGS")
- print(f"{'='*60}")
- saved_files = splitter.save_configs(configs, args.output_dir)
-
- print(f"\n{'='*60}")
- print("NEXT STEPS")
- print(f"{'='*60}")
- print("1. Review generated configs")
- print("2. Scrape each config:")
- for filepath in saved_files:
- print(f" skill-seekers scrape --config {filepath}")
- print("3. Package skills:")
- print(" skill-seekers-package-multi configs/-*.json")
- print("")
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/test_unified_simple.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/test_unified_simple.py
deleted file mode 100644
index f759fd1..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/test_unified_simple.py
+++ /dev/null
@@ -1,192 +0,0 @@
-#!/usr/bin/env python3
-"""
-Simple Integration Tests for Unified Multi-Source Scraper
-
-Focuses on real-world usage patterns rather than unit tests.
-"""
-
-import os
-import sys
-import json
-import tempfile
-from pathlib import Path
-
-# Add CLI to path
-sys.path.insert(0, str(Path(__file__).parent))
-
-from .config_validator import validate_config
-
-def test_validate_existing_unified_configs():
- """Test that all existing unified configs are valid"""
- configs_dir = Path(__file__).parent.parent / 'configs'
-
- unified_configs = [
- 'godot_unified.json',
- 'react_unified.json',
- 'django_unified.json',
- 'fastapi_unified.json'
- ]
-
- for config_name in unified_configs:
- config_path = configs_dir / config_name
- if config_path.exists():
- print(f"\n✓ Validating {config_name}...")
- validator = validate_config(str(config_path))
- assert validator.is_unified, f"{config_name} should be unified format"
- assert validator.needs_api_merge(), f"{config_name} should need API merging"
- print(f" Sources: {len(validator.config['sources'])}")
- print(f" Merge mode: {validator.config.get('merge_mode')}")
-
-
-def test_backward_compatibility():
- """Test that legacy configs still work"""
- configs_dir = Path(__file__).parent.parent / 'configs'
-
- legacy_configs = [
- 'react.json',
- 'godot.json',
- 'django.json'
- ]
-
- for config_name in legacy_configs:
- config_path = configs_dir / config_name
- if config_path.exists():
- print(f"\n✓ Validating legacy {config_name}...")
- validator = validate_config(str(config_path))
- assert not validator.is_unified, f"{config_name} should be legacy format"
- print(f" Format: Legacy")
-
-
-def test_create_temp_unified_config():
- """Test creating a unified config from scratch"""
- config = {
- "name": "test_unified",
- "description": "Test unified config",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://example.com/docs",
- "extract_api": True,
- "max_pages": 50
- },
- {
- "type": "github",
- "repo": "test/repo",
- "include_code": True,
- "code_analysis_depth": "surface"
- }
- ]
- }
-
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(config, f)
- config_path = f.name
-
- try:
- print("\n✓ Validating temp unified config...")
- validator = validate_config(config_path)
- assert validator.is_unified
- assert validator.needs_api_merge()
- assert len(validator.config['sources']) == 2
- print(" ✓ Config is valid unified format")
- print(f" Sources: {len(validator.config['sources'])}")
- finally:
- os.unlink(config_path)
-
-
-def test_mixed_source_types():
- """Test config with documentation, GitHub, and PDF sources"""
- config = {
- "name": "test_mixed",
- "description": "Test mixed sources",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://example.com"
- },
- {
- "type": "github",
- "repo": "test/repo"
- },
- {
- "type": "pdf",
- "path": "/path/to/manual.pdf"
- }
- ]
- }
-
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(config, f)
- config_path = f.name
-
- try:
- print("\n✓ Validating mixed source types...")
- validator = validate_config(config_path)
- assert validator.is_unified
- assert len(validator.config['sources']) == 3
-
- # Check each source type
- source_types = [s['type'] for s in validator.config['sources']]
- assert 'documentation' in source_types
- assert 'github' in source_types
- assert 'pdf' in source_types
- print(" ✓ All 3 source types validated")
- finally:
- os.unlink(config_path)
-
-
-def test_config_validation_errors():
- """Test that invalid configs are rejected"""
- # Invalid source type
- config = {
- "name": "test",
- "description": "Test",
- "sources": [
- {"type": "invalid_type", "url": "https://example.com"}
- ]
- }
-
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(config, f)
- config_path = f.name
-
- try:
- print("\n✓ Testing invalid source type...")
- try:
- # validate_config() calls .validate() automatically
- validator = validate_config(config_path)
- assert False, "Should have raised error for invalid source type"
- except ValueError as e:
- assert "Invalid" in str(e) or "invalid" in str(e)
- print(" ✓ Invalid source type correctly rejected")
- finally:
- os.unlink(config_path)
-
-
-# Run tests
-if __name__ == '__main__':
- print("=" * 60)
- print("Running Unified Scraper Integration Tests")
- print("=" * 60)
-
- try:
- test_validate_existing_unified_configs()
- test_backward_compatibility()
- test_create_temp_unified_config()
- test_mixed_source_types()
- test_config_validation_errors()
-
- print("\n" + "=" * 60)
- print("✅ All integration tests passed!")
- print("=" * 60)
-
- except AssertionError as e:
- print(f"\n❌ Test failed: {e}")
- sys.exit(1)
- except Exception as e:
- print(f"\n❌ Unexpected error: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/unified_scraper.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/unified_scraper.py
deleted file mode 100644
index 81d2bc1..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/unified_scraper.py
+++ /dev/null
@@ -1,450 +0,0 @@
-#!/usr/bin/env python3
-"""
-Unified Multi-Source Scraper
-
-Orchestrates scraping from multiple sources (documentation, GitHub, PDF),
-detects conflicts, merges intelligently, and builds unified skills.
-
-This is the main entry point for unified config workflow.
-
-Usage:
- skill-seekers unified --config configs/godot_unified.json
- skill-seekers unified --config configs/react_unified.json --merge-mode claude-enhanced
-"""
-
-import os
-import sys
-import json
-import logging
-import argparse
-import subprocess
-from pathlib import Path
-from typing import Dict, List, Any, Optional
-
-# Import validators and scrapers
-try:
- from config_validator import ConfigValidator, validate_config
- from conflict_detector import ConflictDetector
- from merge_sources import RuleBasedMerger, ClaudeEnhancedMerger
- from unified_skill_builder import UnifiedSkillBuilder
-except ImportError as e:
- print(f"Error importing modules: {e}")
- print("Make sure you're running from the project root directory")
- sys.exit(1)
-
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-
-class UnifiedScraper:
- """
- Orchestrates multi-source scraping and merging.
-
- Main workflow:
- 1. Load and validate unified config
- 2. Scrape all sources (docs, GitHub, PDF)
- 3. Detect conflicts between sources
- 4. Merge intelligently (rule-based or Claude-enhanced)
- 5. Build unified skill
- """
-
- def __init__(self, config_path: str, merge_mode: Optional[str] = None):
- """
- Initialize unified scraper.
-
- Args:
- config_path: Path to unified config JSON
- merge_mode: Override config merge_mode ('rule-based' or 'claude-enhanced')
- """
- self.config_path = config_path
-
- # Validate and load config
- logger.info(f"Loading config: {config_path}")
- self.validator = validate_config(config_path)
- self.config = self.validator.config
-
- # Determine merge mode
- self.merge_mode = merge_mode or self.config.get('merge_mode', 'rule-based')
- logger.info(f"Merge mode: {self.merge_mode}")
-
- # Storage for scraped data
- self.scraped_data = {}
-
- # Output paths
- self.name = self.config['name']
- self.output_dir = f"output/{self.name}"
- self.data_dir = f"output/{self.name}_unified_data"
-
- os.makedirs(self.output_dir, exist_ok=True)
- os.makedirs(self.data_dir, exist_ok=True)
-
- def scrape_all_sources(self):
- """
- Scrape all configured sources.
-
- Routes to appropriate scraper based on source type.
- """
- logger.info("=" * 60)
- logger.info("PHASE 1: Scraping all sources")
- logger.info("=" * 60)
-
- if not self.validator.is_unified:
- logger.warning("Config is not unified format, converting...")
- self.config = self.validator.convert_legacy_to_unified()
-
- sources = self.config.get('sources', [])
-
- for i, source in enumerate(sources):
- source_type = source['type']
- logger.info(f"\n[{i+1}/{len(sources)}] Scraping {source_type} source...")
-
- try:
- if source_type == 'documentation':
- self._scrape_documentation(source)
- elif source_type == 'github':
- self._scrape_github(source)
- elif source_type == 'pdf':
- self._scrape_pdf(source)
- else:
- logger.warning(f"Unknown source type: {source_type}")
- except Exception as e:
- logger.error(f"Error scraping {source_type}: {e}")
- logger.info("Continuing with other sources...")
-
- logger.info(f"\n✅ Scraped {len(self.scraped_data)} sources successfully")
-
- def _scrape_documentation(self, source: Dict[str, Any]):
- """Scrape documentation website."""
- # Create temporary config for doc scraper
- doc_config = {
- 'name': f"{self.name}_docs",
- 'base_url': source['base_url'],
- 'selectors': source.get('selectors', {}),
- 'url_patterns': source.get('url_patterns', {}),
- 'categories': source.get('categories', {}),
- 'rate_limit': source.get('rate_limit', 0.5),
- 'max_pages': source.get('max_pages', 100)
- }
-
- # Write temporary config
- temp_config_path = os.path.join(self.data_dir, 'temp_docs_config.json')
- with open(temp_config_path, 'w') as f:
- json.dump(doc_config, f, indent=2)
-
- # Run doc_scraper as subprocess
- logger.info(f"Scraping documentation from {source['base_url']}")
-
- doc_scraper_path = Path(__file__).parent / "doc_scraper.py"
- cmd = [sys.executable, str(doc_scraper_path), '--config', temp_config_path]
-
- result = subprocess.run(cmd, capture_output=True, text=True)
-
- if result.returncode != 0:
- logger.error(f"Documentation scraping failed: {result.stderr}")
- return
-
- # Load scraped data
- docs_data_file = f"output/{doc_config['name']}_data/summary.json"
-
- if os.path.exists(docs_data_file):
- with open(docs_data_file, 'r') as f:
- summary = json.load(f)
-
- self.scraped_data['documentation'] = {
- 'pages': summary.get('pages', []),
- 'data_file': docs_data_file
- }
-
- logger.info(f"✅ Documentation: {summary.get('total_pages', 0)} pages scraped")
- else:
- logger.warning("Documentation data file not found")
-
- # Clean up temp config
- if os.path.exists(temp_config_path):
- os.remove(temp_config_path)
-
- def _scrape_github(self, source: Dict[str, Any]):
- """Scrape GitHub repository."""
- sys.path.insert(0, str(Path(__file__).parent))
-
- try:
- from github_scraper import GitHubScraper
- except ImportError:
- logger.error("github_scraper.py not found")
- return
-
- # Create config for GitHub scraper
- github_config = {
- 'repo': source['repo'],
- 'name': f"{self.name}_github",
- 'github_token': source.get('github_token'),
- 'include_issues': source.get('include_issues', True),
- 'max_issues': source.get('max_issues', 100),
- 'include_changelog': source.get('include_changelog', True),
- 'include_releases': source.get('include_releases', True),
- 'include_code': source.get('include_code', True),
- 'code_analysis_depth': source.get('code_analysis_depth', 'surface'),
- 'file_patterns': source.get('file_patterns', []),
- 'local_repo_path': source.get('local_repo_path') # Pass local_repo_path from config
- }
-
- # Scrape
- logger.info(f"Scraping GitHub repository: {source['repo']}")
- scraper = GitHubScraper(github_config)
- github_data = scraper.scrape()
-
- # Save data
- github_data_file = os.path.join(self.data_dir, 'github_data.json')
- with open(github_data_file, 'w') as f:
- json.dump(github_data, f, indent=2, ensure_ascii=False)
-
- self.scraped_data['github'] = {
- 'data': github_data,
- 'data_file': github_data_file
- }
-
- logger.info(f"✅ GitHub: Repository scraped successfully")
-
- def _scrape_pdf(self, source: Dict[str, Any]):
- """Scrape PDF document."""
- sys.path.insert(0, str(Path(__file__).parent))
-
- try:
- from pdf_scraper import PDFToSkillConverter
- except ImportError:
- logger.error("pdf_scraper.py not found")
- return
-
- # Create config for PDF scraper
- pdf_config = {
- 'name': f"{self.name}_pdf",
- 'pdf': source['path'],
- 'extract_tables': source.get('extract_tables', False),
- 'ocr': source.get('ocr', False),
- 'password': source.get('password')
- }
-
- # Scrape
- logger.info(f"Scraping PDF: {source['path']}")
- converter = PDFToSkillConverter(pdf_config)
- pdf_data = converter.extract_all()
-
- # Save data
- pdf_data_file = os.path.join(self.data_dir, 'pdf_data.json')
- with open(pdf_data_file, 'w') as f:
- json.dump(pdf_data, f, indent=2, ensure_ascii=False)
-
- self.scraped_data['pdf'] = {
- 'data': pdf_data,
- 'data_file': pdf_data_file
- }
-
- logger.info(f"✅ PDF: {len(pdf_data.get('pages', []))} pages extracted")
-
- def detect_conflicts(self) -> List:
- """
- Detect conflicts between documentation and code.
-
- Only applicable if both documentation and GitHub sources exist.
-
- Returns:
- List of conflicts
- """
- logger.info("\n" + "=" * 60)
- logger.info("PHASE 2: Detecting conflicts")
- logger.info("=" * 60)
-
- if not self.validator.needs_api_merge():
- logger.info("No API merge needed (only one API source)")
- return []
-
- # Get documentation and GitHub data
- docs_data = self.scraped_data.get('documentation', {})
- github_data = self.scraped_data.get('github', {})
-
- if not docs_data or not github_data:
- logger.warning("Missing documentation or GitHub data for conflict detection")
- return []
-
- # Load data files
- with open(docs_data['data_file'], 'r') as f:
- docs_json = json.load(f)
-
- with open(github_data['data_file'], 'r') as f:
- github_json = json.load(f)
-
- # Detect conflicts
- detector = ConflictDetector(docs_json, github_json)
- conflicts = detector.detect_all_conflicts()
-
- # Save conflicts
- conflicts_file = os.path.join(self.data_dir, 'conflicts.json')
- detector.save_conflicts(conflicts, conflicts_file)
-
- # Print summary
- summary = detector.generate_summary(conflicts)
- logger.info(f"\n📊 Conflict Summary:")
- logger.info(f" Total: {summary['total']}")
- logger.info(f" By Type:")
- for ctype, count in summary['by_type'].items():
- if count > 0:
- logger.info(f" - {ctype}: {count}")
- logger.info(f" By Severity:")
- for severity, count in summary['by_severity'].items():
- if count > 0:
- emoji = '🔴' if severity == 'high' else '🟡' if severity == 'medium' else '🟢'
- logger.info(f" {emoji} {severity}: {count}")
-
- return conflicts
-
- def merge_sources(self, conflicts: List):
- """
- Merge data from multiple sources.
-
- Args:
- conflicts: List of detected conflicts
- """
- logger.info("\n" + "=" * 60)
- logger.info(f"PHASE 3: Merging sources ({self.merge_mode})")
- logger.info("=" * 60)
-
- if not conflicts:
- logger.info("No conflicts to merge")
- return None
-
- # Get data files
- docs_data = self.scraped_data.get('documentation', {})
- github_data = self.scraped_data.get('github', {})
-
- # Load data
- with open(docs_data['data_file'], 'r') as f:
- docs_json = json.load(f)
-
- with open(github_data['data_file'], 'r') as f:
- github_json = json.load(f)
-
- # Choose merger
- if self.merge_mode == 'claude-enhanced':
- merger = ClaudeEnhancedMerger(docs_json, github_json, conflicts)
- else:
- merger = RuleBasedMerger(docs_json, github_json, conflicts)
-
- # Merge
- merged_data = merger.merge_all()
-
- # Save merged data
- merged_file = os.path.join(self.data_dir, 'merged_data.json')
- with open(merged_file, 'w') as f:
- json.dump(merged_data, f, indent=2, ensure_ascii=False)
-
- logger.info(f"✅ Merged data saved: {merged_file}")
-
- return merged_data
-
- def build_skill(self, merged_data: Optional[Dict] = None):
- """
- Build final unified skill.
-
- Args:
- merged_data: Merged API data (if conflicts were resolved)
- """
- logger.info("\n" + "=" * 60)
- logger.info("PHASE 4: Building unified skill")
- logger.info("=" * 60)
-
- # Load conflicts if they exist
- conflicts = []
- conflicts_file = os.path.join(self.data_dir, 'conflicts.json')
- if os.path.exists(conflicts_file):
- with open(conflicts_file, 'r') as f:
- conflicts_data = json.load(f)
- conflicts = conflicts_data.get('conflicts', [])
-
- # Build skill
- builder = UnifiedSkillBuilder(
- self.config,
- self.scraped_data,
- merged_data,
- conflicts
- )
-
- builder.build()
-
- logger.info(f"✅ Unified skill built: {self.output_dir}/")
-
- def run(self):
- """
- Execute complete unified scraping workflow.
- """
- logger.info("\n" + "🚀 " * 20)
- logger.info(f"Unified Scraper: {self.config['name']}")
- logger.info("🚀 " * 20 + "\n")
-
- try:
- # Phase 1: Scrape all sources
- self.scrape_all_sources()
-
- # Phase 2: Detect conflicts (if applicable)
- conflicts = self.detect_conflicts()
-
- # Phase 3: Merge sources (if conflicts exist)
- merged_data = None
- if conflicts:
- merged_data = self.merge_sources(conflicts)
-
- # Phase 4: Build skill
- self.build_skill(merged_data)
-
- logger.info("\n" + "✅ " * 20)
- logger.info("Unified scraping complete!")
- logger.info("✅ " * 20 + "\n")
-
- logger.info(f"📁 Output: {self.output_dir}/")
- logger.info(f"📁 Data: {self.data_dir}/")
-
- except KeyboardInterrupt:
- logger.info("\n\n⚠️ Scraping interrupted by user")
- sys.exit(1)
- except Exception as e:
- logger.error(f"\n\n❌ Error during scraping: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
-
-
-def main():
- """Main entry point."""
- parser = argparse.ArgumentParser(
- description='Unified multi-source scraper',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Basic usage with unified config
- skill-seekers unified --config configs/godot_unified.json
-
- # Override merge mode
- skill-seekers unified --config configs/react_unified.json --merge-mode claude-enhanced
-
- # Backward compatible with legacy configs
- skill-seekers unified --config configs/react.json
- """
- )
-
- parser.add_argument('--config', '-c', required=True,
- help='Path to unified config JSON file')
- parser.add_argument('--merge-mode', '-m',
- choices=['rule-based', 'claude-enhanced'],
- help='Override config merge mode')
-
- args = parser.parse_args()
-
- # Create and run scraper
- scraper = UnifiedScraper(args.config, args.merge_mode)
- scraper.run()
-
-
-if __name__ == '__main__':
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/unified_skill_builder.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/unified_skill_builder.py
deleted file mode 100644
index dd3051d..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/unified_skill_builder.py
+++ /dev/null
@@ -1,444 +0,0 @@
-#!/usr/bin/env python3
-"""
-Unified Skill Builder
-
-Generates final skill structure from merged multi-source data:
-- SKILL.md with merged APIs and conflict warnings
-- references/ with organized content by source
-- Inline conflict markers (⚠️)
-- Separate conflicts summary section
-
-Supports mixed sources (documentation, GitHub, PDF) and highlights
-discrepancies transparently.
-"""
-
-import os
-import json
-import logging
-from pathlib import Path
-from typing import Dict, List, Any, Optional
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-class UnifiedSkillBuilder:
- """
- Builds unified skill from multi-source data.
- """
-
- def __init__(self, config: Dict, scraped_data: Dict,
- merged_data: Optional[Dict] = None, conflicts: Optional[List] = None):
- """
- Initialize skill builder.
-
- Args:
- config: Unified config dict
- scraped_data: Dict of scraped data by source type
- merged_data: Merged API data (if conflicts were resolved)
- conflicts: List of detected conflicts
- """
- self.config = config
- self.scraped_data = scraped_data
- self.merged_data = merged_data
- self.conflicts = conflicts or []
-
- self.name = config['name']
- self.description = config['description']
- self.skill_dir = f"output/{self.name}"
-
- # Create directories
- os.makedirs(self.skill_dir, exist_ok=True)
- os.makedirs(f"{self.skill_dir}/references", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/scripts", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/assets", exist_ok=True)
-
- def build(self):
- """Build complete skill structure."""
- logger.info(f"Building unified skill: {self.name}")
-
- # Generate main SKILL.md
- self._generate_skill_md()
-
- # Generate reference files by source
- self._generate_references()
-
- # Generate conflicts report (if any)
- if self.conflicts:
- self._generate_conflicts_report()
-
- logger.info(f"✅ Unified skill built: {self.skill_dir}/")
-
- def _generate_skill_md(self):
- """Generate main SKILL.md file."""
- skill_path = os.path.join(self.skill_dir, 'SKILL.md')
-
- # Generate skill name (lowercase, hyphens only, max 64 chars)
- skill_name = self.name.lower().replace('_', '-').replace(' ', '-')[:64]
-
- # Truncate description to 1024 chars if needed
- desc = self.description[:1024] if len(self.description) > 1024 else self.description
-
- content = f"""---
-name: {skill_name}
-description: {desc}
----
-
-# {self.name.title()}
-
-{self.description}
-
-## 📚 Sources
-
-This skill combines knowledge from multiple sources:
-
-"""
-
- # List sources
- for source in self.config.get('sources', []):
- source_type = source['type']
- if source_type == 'documentation':
- content += f"- ✅ **Documentation**: {source.get('base_url', 'N/A')}\n"
- content += f" - Pages: {source.get('max_pages', 'unlimited')}\n"
- elif source_type == 'github':
- content += f"- ✅ **GitHub Repository**: {source.get('repo', 'N/A')}\n"
- content += f" - Code Analysis: {source.get('code_analysis_depth', 'surface')}\n"
- content += f" - Issues: {source.get('max_issues', 0)}\n"
- elif source_type == 'pdf':
- content += f"- ✅ **PDF Document**: {source.get('path', 'N/A')}\n"
-
- # Data quality section
- if self.conflicts:
- content += f"\n## ⚠️ Data Quality\n\n"
- content += f"**{len(self.conflicts)} conflicts detected** between sources.\n\n"
-
- # Count by type
- by_type = {}
- for conflict in self.conflicts:
- ctype = conflict.type if hasattr(conflict, 'type') else conflict.get('type', 'unknown')
- by_type[ctype] = by_type.get(ctype, 0) + 1
-
- content += "**Conflict Breakdown:**\n"
- for ctype, count in by_type.items():
- content += f"- {ctype}: {count}\n"
-
- content += f"\nSee `references/conflicts.md` for detailed conflict information.\n"
-
- # Merged API section (if available)
- if self.merged_data:
- content += self._format_merged_apis()
-
- # Quick reference from each source
- content += "\n## 📖 Reference Documentation\n\n"
- content += "Organized by source:\n\n"
-
- for source in self.config.get('sources', []):
- source_type = source['type']
- content += f"- [{source_type.title()}](references/{source_type}/)\n"
-
- # When to use this skill
- content += f"\n## 💡 When to Use This Skill\n\n"
- content += f"Use this skill when you need to:\n"
- content += f"- Understand how to use {self.name}\n"
- content += f"- Look up API documentation\n"
- content += f"- Find usage examples\n"
-
- if 'github' in self.scraped_data:
- content += f"- Check for known issues or recent changes\n"
- content += f"- Review release history\n"
-
- content += "\n---\n\n"
- content += "*Generated by Skill Seeker's unified multi-source scraper*\n"
-
- with open(skill_path, 'w', encoding='utf-8') as f:
- f.write(content)
-
- logger.info(f"Created SKILL.md")
-
- def _format_merged_apis(self) -> str:
- """Format merged APIs section with inline conflict warnings."""
- if not self.merged_data:
- return ""
-
- content = "\n## 🔧 API Reference\n\n"
- content += "*Merged from documentation and code analysis*\n\n"
-
- apis = self.merged_data.get('apis', {})
-
- if not apis:
- return content + "*No APIs to display*\n"
-
- # Group APIs by status
- matched = {k: v for k, v in apis.items() if v.get('status') == 'matched'}
- conflicts = {k: v for k, v in apis.items() if v.get('status') == 'conflict'}
- docs_only = {k: v for k, v in apis.items() if v.get('status') == 'docs_only'}
- code_only = {k: v for k, v in apis.items() if v.get('status') == 'code_only'}
-
- # Show matched APIs first
- if matched:
- content += "### ✅ Verified APIs\n\n"
- content += "*Documentation and code agree*\n\n"
- for api_name, api_data in list(matched.items())[:10]: # Limit to first 10
- content += self._format_api_entry(api_data, inline_conflict=False)
-
- # Show conflicting APIs with warnings
- if conflicts:
- content += "\n### ⚠️ APIs with Conflicts\n\n"
- content += "*Documentation and code differ*\n\n"
- for api_name, api_data in list(conflicts.items())[:10]:
- content += self._format_api_entry(api_data, inline_conflict=True)
-
- # Show undocumented APIs
- if code_only:
- content += f"\n### 💻 Undocumented APIs\n\n"
- content += f"*Found in code but not in documentation ({len(code_only)} total)*\n\n"
- for api_name, api_data in list(code_only.items())[:5]:
- content += self._format_api_entry(api_data, inline_conflict=False)
-
- # Show removed/missing APIs
- if docs_only:
- content += f"\n### 📖 Documentation-Only APIs\n\n"
- content += f"*Documented but not found in code ({len(docs_only)} total)*\n\n"
- for api_name, api_data in list(docs_only.items())[:5]:
- content += self._format_api_entry(api_data, inline_conflict=False)
-
- content += f"\n*See references/api/ for complete API documentation*\n"
-
- return content
-
- def _format_api_entry(self, api_data: Dict, inline_conflict: bool = False) -> str:
- """Format a single API entry."""
- name = api_data.get('name', 'Unknown')
- signature = api_data.get('merged_signature', name)
- description = api_data.get('merged_description', '')
- warning = api_data.get('warning', '')
-
- entry = f"#### `{signature}`\n\n"
-
- if description:
- entry += f"{description}\n\n"
-
- # Add inline conflict warning
- if inline_conflict and warning:
- entry += f"⚠️ **Conflict**: {warning}\n\n"
-
- # Show both versions if available
- conflict = api_data.get('conflict', {})
- if conflict:
- docs_info = conflict.get('docs_info')
- code_info = conflict.get('code_info')
-
- if docs_info and code_info:
- entry += "**Documentation says:**\n"
- entry += f"```\n{docs_info.get('raw_signature', 'N/A')}\n```\n\n"
- entry += "**Code implementation:**\n"
- entry += f"```\n{self._format_code_signature(code_info)}\n```\n\n"
-
- # Add source info
- source = api_data.get('source', 'unknown')
- entry += f"*Source: {source}*\n\n"
-
- entry += "---\n\n"
-
- return entry
-
- def _format_code_signature(self, code_info: Dict) -> str:
- """Format code signature for display."""
- name = code_info.get('name', '')
- params = code_info.get('parameters', [])
- return_type = code_info.get('return_type')
-
- param_strs = []
- for param in params:
- param_str = param.get('name', '')
- if param.get('type_hint'):
- param_str += f": {param['type_hint']}"
- if param.get('default'):
- param_str += f" = {param['default']}"
- param_strs.append(param_str)
-
- sig = f"{name}({', '.join(param_strs)})"
- if return_type:
- sig += f" -> {return_type}"
-
- return sig
-
- def _generate_references(self):
- """Generate reference files organized by source."""
- logger.info("Generating reference files...")
-
- # Generate references for each source type
- if 'documentation' in self.scraped_data:
- self._generate_docs_references()
-
- if 'github' in self.scraped_data:
- self._generate_github_references()
-
- if 'pdf' in self.scraped_data:
- self._generate_pdf_references()
-
- # Generate merged API reference if available
- if self.merged_data:
- self._generate_merged_api_reference()
-
- def _generate_docs_references(self):
- """Generate references from documentation source."""
- docs_dir = os.path.join(self.skill_dir, 'references', 'documentation')
- os.makedirs(docs_dir, exist_ok=True)
-
- # Create index
- index_path = os.path.join(docs_dir, 'index.md')
- with open(index_path, 'w') as f:
- f.write("# Documentation\n\n")
- f.write("Reference from official documentation.\n\n")
-
- logger.info("Created documentation references")
-
- def _generate_github_references(self):
- """Generate references from GitHub source."""
- github_dir = os.path.join(self.skill_dir, 'references', 'github')
- os.makedirs(github_dir, exist_ok=True)
-
- github_data = self.scraped_data['github']['data']
-
- # Create README reference
- if github_data.get('readme'):
- readme_path = os.path.join(github_dir, 'README.md')
- with open(readme_path, 'w') as f:
- f.write("# Repository README\n\n")
- f.write(github_data['readme'])
-
- # Create issues reference
- if github_data.get('issues'):
- issues_path = os.path.join(github_dir, 'issues.md')
- with open(issues_path, 'w') as f:
- f.write("# GitHub Issues\n\n")
- f.write(f"{len(github_data['issues'])} recent issues.\n\n")
-
- for issue in github_data['issues'][:20]:
- f.write(f"## #{issue['number']}: {issue['title']}\n\n")
- f.write(f"**State**: {issue['state']}\n")
- if issue.get('labels'):
- f.write(f"**Labels**: {', '.join(issue['labels'])}\n")
- f.write(f"**URL**: {issue.get('url', 'N/A')}\n\n")
-
- # Create releases reference
- if github_data.get('releases'):
- releases_path = os.path.join(github_dir, 'releases.md')
- with open(releases_path, 'w') as f:
- f.write("# Releases\n\n")
-
- for release in github_data['releases'][:10]:
- f.write(f"## {release['tag_name']}: {release.get('name', 'N/A')}\n\n")
- f.write(f"**Published**: {release.get('published_at', 'N/A')[:10]}\n\n")
- if release.get('body'):
- f.write(release['body'][:500])
- f.write("\n\n")
-
- logger.info("Created GitHub references")
-
- def _generate_pdf_references(self):
- """Generate references from PDF source."""
- pdf_dir = os.path.join(self.skill_dir, 'references', 'pdf')
- os.makedirs(pdf_dir, exist_ok=True)
-
- # Create index
- index_path = os.path.join(pdf_dir, 'index.md')
- with open(index_path, 'w') as f:
- f.write("# PDF Documentation\n\n")
- f.write("Reference from PDF document.\n\n")
-
- logger.info("Created PDF references")
-
- def _generate_merged_api_reference(self):
- """Generate merged API reference file."""
- api_dir = os.path.join(self.skill_dir, 'references', 'api')
- os.makedirs(api_dir, exist_ok=True)
-
- api_path = os.path.join(api_dir, 'merged_api.md')
-
- with open(api_path, 'w') as f:
- f.write("# Merged API Reference\n\n")
- f.write("*Combined from documentation and code analysis*\n\n")
-
- apis = self.merged_data.get('apis', {})
-
- for api_name in sorted(apis.keys()):
- api_data = apis[api_name]
- entry = self._format_api_entry(api_data, inline_conflict=True)
- f.write(entry)
-
- logger.info(f"Created merged API reference ({len(apis)} APIs)")
-
- def _generate_conflicts_report(self):
- """Generate detailed conflicts report."""
- conflicts_path = os.path.join(self.skill_dir, 'references', 'conflicts.md')
-
- with open(conflicts_path, 'w') as f:
- f.write("# Conflict Report\n\n")
- f.write(f"Found **{len(self.conflicts)}** conflicts between sources.\n\n")
-
- # Group by severity
- high = [c for c in self.conflicts if (hasattr(c, 'severity') and c.severity == 'high') or c.get('severity') == 'high']
- medium = [c for c in self.conflicts if (hasattr(c, 'severity') and c.severity == 'medium') or c.get('severity') == 'medium']
- low = [c for c in self.conflicts if (hasattr(c, 'severity') and c.severity == 'low') or c.get('severity') == 'low']
-
- f.write("## Severity Breakdown\n\n")
- f.write(f"- 🔴 **High**: {len(high)} (action required)\n")
- f.write(f"- 🟡 **Medium**: {len(medium)} (review recommended)\n")
- f.write(f"- 🟢 **Low**: {len(low)} (informational)\n\n")
-
- # List high severity conflicts
- if high:
- f.write("## 🔴 High Severity\n\n")
- f.write("*These conflicts require immediate attention*\n\n")
-
- for conflict in high:
- api_name = conflict.api_name if hasattr(conflict, 'api_name') else conflict.get('api_name', 'Unknown')
- diff = conflict.difference if hasattr(conflict, 'difference') else conflict.get('difference', 'N/A')
-
- f.write(f"### {api_name}\n\n")
- f.write(f"**Issue**: {diff}\n\n")
-
- # List medium severity
- if medium:
- f.write("## 🟡 Medium Severity\n\n")
-
- for conflict in medium[:20]: # Limit to 20
- api_name = conflict.api_name if hasattr(conflict, 'api_name') else conflict.get('api_name', 'Unknown')
- diff = conflict.difference if hasattr(conflict, 'difference') else conflict.get('difference', 'N/A')
-
- f.write(f"### {api_name}\n\n")
- f.write(f"{diff}\n\n")
-
- logger.info(f"Created conflicts report")
-
-
-if __name__ == '__main__':
- # Test with mock data
- import sys
-
- if len(sys.argv) < 2:
- print("Usage: python unified_skill_builder.py ")
- sys.exit(1)
-
- config_path = sys.argv[1]
-
- with open(config_path, 'r') as f:
- config = json.load(f)
-
- # Mock scraped data
- scraped_data = {
- 'github': {
- 'data': {
- 'readme': '# Test Repository',
- 'issues': [],
- 'releases': []
- }
- }
- }
-
- builder = UnifiedSkillBuilder(config, scraped_data)
- builder.build()
-
- print(f"\n✅ Test skill built in: output/{config['name']}/")
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/upload_skill.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/upload_skill.py
deleted file mode 100644
index 0694195..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/upload_skill.py
+++ /dev/null
@@ -1,175 +0,0 @@
-#!/usr/bin/env python3
-"""
-Automatic Skill Uploader
-Uploads a skill .zip file to Claude using the Anthropic API
-
-Usage:
- # Set API key (one-time)
- export ANTHROPIC_API_KEY=sk-ant-...
-
- # Upload skill
- python3 upload_skill.py output/react.zip
- python3 upload_skill.py output/godot.zip
-"""
-
-import os
-import sys
-import json
-import argparse
-from pathlib import Path
-
-# Import utilities
-try:
- from utils import (
- get_api_key,
- get_upload_url,
- print_upload_instructions,
- validate_zip_file
- )
-except ImportError:
- sys.path.insert(0, str(Path(__file__).parent))
- from utils import (
- get_api_key,
- get_upload_url,
- print_upload_instructions,
- validate_zip_file
- )
-
-
-def upload_skill_api(zip_path):
- """
- Upload skill to Claude via Anthropic API
-
- Args:
- zip_path: Path to skill .zip file
-
- Returns:
- tuple: (success, message)
- """
- # Check for requests library
- try:
- import requests
- except ImportError:
- return False, "requests library not installed. Run: pip install requests"
-
- # Validate zip file
- is_valid, error_msg = validate_zip_file(zip_path)
- if not is_valid:
- return False, error_msg
-
- # Get API key
- api_key = get_api_key()
- if not api_key:
- return False, "ANTHROPIC_API_KEY not set. Run: export ANTHROPIC_API_KEY=sk-ant-..."
-
- zip_path = Path(zip_path)
- skill_name = zip_path.stem
-
- print(f"📤 Uploading skill: {skill_name}")
- print(f" Source: {zip_path}")
- print(f" Size: {zip_path.stat().st_size:,} bytes")
- print()
-
- # Prepare API request
- api_url = "https://api.anthropic.com/v1/skills"
- headers = {
- "x-api-key": api_key,
- "anthropic-version": "2023-06-01",
- "anthropic-beta": "skills-2025-10-02"
- }
-
- try:
- # Read zip file
- with open(zip_path, 'rb') as f:
- zip_data = f.read()
-
- # Upload skill
- print("⏳ Uploading to Anthropic API...")
-
- files = {
- 'files[]': (zip_path.name, zip_data, 'application/zip')
- }
-
- response = requests.post(
- api_url,
- headers=headers,
- files=files,
- timeout=60
- )
-
- # Check response
- if response.status_code == 200:
- print()
- print("✅ Skill uploaded successfully!")
- print()
- print("Your skill is now available in Claude at:")
- print(f" {get_upload_url()}")
- print()
- return True, "Upload successful"
-
- elif response.status_code == 401:
- return False, "Authentication failed. Check your ANTHROPIC_API_KEY"
-
- elif response.status_code == 400:
- error_msg = response.json().get('error', {}).get('message', 'Unknown error')
- return False, f"Invalid skill format: {error_msg}"
-
- else:
- error_msg = response.json().get('error', {}).get('message', 'Unknown error')
- return False, f"Upload failed ({response.status_code}): {error_msg}"
-
- except requests.exceptions.Timeout:
- return False, "Upload timed out. Try again or use manual upload"
-
- except requests.exceptions.ConnectionError:
- return False, "Connection error. Check your internet connection"
-
- except Exception as e:
- return False, f"Unexpected error: {str(e)}"
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Upload a skill .zip file to Claude via Anthropic API",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Setup:
- 1. Get your Anthropic API key from https://console.anthropic.com/
- 2. Set the API key:
- export ANTHROPIC_API_KEY=sk-ant-...
-
-Examples:
- # Upload skill
- python3 upload_skill.py output/react.zip
-
- # Upload with explicit path
- python3 upload_skill.py /path/to/skill.zip
-
-Requirements:
- - ANTHROPIC_API_KEY environment variable must be set
- - requests library (pip install requests)
- """
- )
-
- parser.add_argument(
- 'zip_file',
- help='Path to skill .zip file (e.g., output/react.zip)'
- )
-
- args = parser.parse_args()
-
- # Upload skill
- success, message = upload_skill_api(args.zip_file)
-
- if success:
- sys.exit(0)
- else:
- print(f"\n❌ Upload failed: {message}")
- print()
- print("📝 Manual upload instructions:")
- print_upload_instructions(args.zip_file)
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/utils.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/utils.py
deleted file mode 100644
index 2432cd1..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/cli/utils.py
+++ /dev/null
@@ -1,224 +0,0 @@
-#!/usr/bin/env python3
-"""
-Utility functions for Skill Seeker CLI tools
-"""
-
-import os
-import sys
-import subprocess
-import platform
-from pathlib import Path
-from typing import Optional, Tuple, Dict, Union
-
-
-def open_folder(folder_path: Union[str, Path]) -> bool:
- """
- Open a folder in the system file browser
-
- Args:
- folder_path: Path to folder to open
-
- Returns:
- bool: True if successful, False otherwise
- """
- folder_path = Path(folder_path).resolve()
-
- if not folder_path.exists():
- print(f"⚠️ Folder not found: {folder_path}")
- return False
-
- system = platform.system()
-
- try:
- if system == "Linux":
- # Try xdg-open first (standard)
- subprocess.run(["xdg-open", str(folder_path)], check=True)
- elif system == "Darwin": # macOS
- subprocess.run(["open", str(folder_path)], check=True)
- elif system == "Windows":
- subprocess.run(["explorer", str(folder_path)], check=True)
- else:
- print(f"⚠️ Unknown operating system: {system}")
- return False
-
- return True
-
- except subprocess.CalledProcessError:
- print(f"⚠️ Could not open folder automatically")
- return False
- except FileNotFoundError:
- print(f"⚠️ File browser not found on system")
- return False
-
-
-def has_api_key() -> bool:
- """
- Check if ANTHROPIC_API_KEY is set in environment
-
- Returns:
- bool: True if API key is set, False otherwise
- """
- api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
- return len(api_key) > 0
-
-
-def get_api_key() -> Optional[str]:
- """
- Get ANTHROPIC_API_KEY from environment
-
- Returns:
- str: API key or None if not set
- """
- api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
- return api_key if api_key else None
-
-
-def get_upload_url() -> str:
- """
- Get the Claude skills upload URL
-
- Returns:
- str: Claude skills upload URL
- """
- return "https://claude.ai/skills"
-
-
-def print_upload_instructions(zip_path: Union[str, Path]) -> None:
- """
- Print clear upload instructions for manual upload
-
- Args:
- zip_path: Path to the .zip file to upload
- """
- zip_path = Path(zip_path)
-
- print()
- print("╔══════════════════════════════════════════════════════════╗")
- print("║ NEXT STEP ║")
- print("╚══════════════════════════════════════════════════════════╝")
- print()
- print(f"📤 Upload to Claude: {get_upload_url()}")
- print()
- print(f"1. Go to {get_upload_url()}")
- print("2. Click \"Upload Skill\"")
- print(f"3. Select: {zip_path}")
- print("4. Done! ✅")
- print()
-
-
-def format_file_size(size_bytes: int) -> str:
- """
- Format file size in human-readable format
-
- Args:
- size_bytes: Size in bytes
-
- Returns:
- str: Formatted size (e.g., "45.3 KB")
- """
- if size_bytes < 1024:
- return f"{size_bytes} bytes"
- elif size_bytes < 1024 * 1024:
- return f"{size_bytes / 1024:.1f} KB"
- else:
- return f"{size_bytes / (1024 * 1024):.1f} MB"
-
-
-def validate_skill_directory(skill_dir: Union[str, Path]) -> Tuple[bool, Optional[str]]:
- """
- Validate that a directory is a valid skill directory
-
- Args:
- skill_dir: Path to skill directory
-
- Returns:
- tuple: (is_valid, error_message)
- """
- skill_path = Path(skill_dir)
-
- if not skill_path.exists():
- return False, f"Directory not found: {skill_dir}"
-
- if not skill_path.is_dir():
- return False, f"Not a directory: {skill_dir}"
-
- skill_md = skill_path / "SKILL.md"
- if not skill_md.exists():
- return False, f"SKILL.md not found in {skill_dir}"
-
- return True, None
-
-
-def validate_zip_file(zip_path: Union[str, Path]) -> Tuple[bool, Optional[str]]:
- """
- Validate that a file is a valid skill .zip file
-
- Args:
- zip_path: Path to .zip file
-
- Returns:
- tuple: (is_valid, error_message)
- """
- zip_path = Path(zip_path)
-
- if not zip_path.exists():
- return False, f"File not found: {zip_path}"
-
- if not zip_path.is_file():
- return False, f"Not a file: {zip_path}"
-
- if not zip_path.suffix == '.zip':
- return False, f"Not a .zip file: {zip_path}"
-
- return True, None
-
-
-def read_reference_files(skill_dir: Union[str, Path], max_chars: int = 100000, preview_limit: int = 40000) -> Dict[str, str]:
- """Read reference files from a skill directory with size limits.
-
- This function reads markdown files from the references/ subdirectory
- of a skill, applying both per-file and total content limits.
-
- Args:
- skill_dir (str or Path): Path to skill directory
- max_chars (int): Maximum total characters to read (default: 100000)
- preview_limit (int): Maximum characters per file (default: 40000)
-
- Returns:
- dict: Dictionary mapping filename to content
-
- Example:
- >>> refs = read_reference_files('output/react/', max_chars=50000)
- >>> len(refs)
- 5
- """
- from pathlib import Path
-
- skill_path = Path(skill_dir)
- references_dir = skill_path / "references"
- references: Dict[str, str] = {}
-
- if not references_dir.exists():
- print(f"⚠ No references directory found at {references_dir}")
- return references
-
- total_chars = 0
- for ref_file in sorted(references_dir.glob("*.md")):
- if ref_file.name == "index.md":
- continue
-
- content = ref_file.read_text(encoding='utf-8')
-
- # Limit size per file
- if len(content) > preview_limit:
- content = content[:preview_limit] + "\n\n[Content truncated...]"
-
- references[ref_file.name] = content
- total_chars += len(content)
-
- # Stop if we've read enough
- if total_chars > max_chars:
- print(f" ℹ Limiting input to {max_chars:,} characters")
- break
-
- return references
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/README.md b/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/README.md
deleted file mode 100644
index f34cb1f..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/README.md
+++ /dev/null
@@ -1,596 +0,0 @@
-# Skill Seeker MCP Server
-
-Model Context Protocol (MCP) server for Skill Seeker - enables Claude Code to generate documentation skills directly.
-
-## What is This?
-
-This MCP server allows Claude Code to use Skill Seeker's tools directly through natural language commands. Instead of running CLI commands manually, you can ask Claude Code to:
-
-- Generate config files for any documentation site
-- Estimate page counts before scraping
-- Scrape documentation and build skills
-- Package skills into `.zip` files
-- List and validate configurations
-- Split large documentation (10K-40K+ pages) into focused sub-skills
-- Generate intelligent router/hub skills for split documentation
-- **NEW:** Scrape PDF documentation and extract code/images
-
-## Quick Start
-
-### 1. Install Dependencies
-
-```bash
-# From repository root
-pip3 install -r mcp/requirements.txt
-pip3 install requests beautifulsoup4
-```
-
-### 2. Quick Setup (Automated)
-
-```bash
-# Run the setup script
-./setup_mcp.sh
-
-# Follow the prompts - it will:
-# - Install dependencies
-# - Test the server
-# - Generate configuration
-# - Guide you through Claude Code setup
-```
-
-### 3. Manual Setup
-
-Add to `~/.config/claude-code/mcp.json`:
-
-```json
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "python3",
- "args": [
- "/path/to/Skill_Seekers/mcp/server.py"
- ],
- "cwd": "/path/to/Skill_Seekers"
- }
- }
-}
-```
-
-**Replace `/path/to/Skill_Seekers`** with your actual repository path!
-
-### 4. Restart Claude Code
-
-Quit and reopen Claude Code (don't just close the window).
-
-### 5. Test
-
-In Claude Code, type:
-```
-List all available configs
-```
-
-You should see a list of preset configurations (Godot, React, Vue, etc.).
-
-## Available Tools
-
-The MCP server exposes 10 tools:
-
-### 1. `generate_config`
-Create a new configuration file for any documentation website.
-
-**Parameters:**
-- `name` (required): Skill name (e.g., "tailwind")
-- `url` (required): Documentation URL (e.g., "https://tailwindcss.com/docs")
-- `description` (required): When to use this skill
-- `max_pages` (optional): Maximum pages to scrape (default: 100)
-- `rate_limit` (optional): Delay between requests in seconds (default: 0.5)
-
-**Example:**
-```
-Generate config for Tailwind CSS at https://tailwindcss.com/docs
-```
-
-### 2. `estimate_pages`
-Estimate how many pages will be scraped from a config (fast, no data downloaded).
-
-**Parameters:**
-- `config_path` (required): Path to config file (e.g., "configs/react.json")
-- `max_discovery` (optional): Maximum pages to discover (default: 1000)
-
-**Example:**
-```
-Estimate pages for configs/react.json
-```
-
-### 3. `scrape_docs`
-Scrape documentation and build Claude skill.
-
-**Parameters:**
-- `config_path` (required): Path to config file
-- `enhance_local` (optional): Open terminal for local enhancement (default: false)
-- `skip_scrape` (optional): Use cached data (default: false)
-- `dry_run` (optional): Preview without saving (default: false)
-
-**Example:**
-```
-Scrape docs using configs/react.json
-```
-
-### 4. `package_skill`
-Package a skill directory into a `.zip` file ready for Claude upload. Automatically uploads if ANTHROPIC_API_KEY is set.
-
-**Parameters:**
-- `skill_dir` (required): Path to skill directory (e.g., "output/react/")
-- `auto_upload` (optional): Try to upload automatically if API key is available (default: true)
-
-**Example:**
-```
-Package skill at output/react/
-```
-
-### 5. `upload_skill`
-Upload a skill .zip file to Claude automatically (requires ANTHROPIC_API_KEY).
-
-**Parameters:**
-- `skill_zip` (required): Path to skill .zip file (e.g., "output/react.zip")
-
-**Example:**
-```
-Upload output/react.zip using upload_skill
-```
-
-### 6. `list_configs`
-List all available preset configurations.
-
-**Parameters:** None
-
-**Example:**
-```
-List all available configs
-```
-
-### 7. `validate_config`
-Validate a config file for errors.
-
-**Parameters:**
-- `config_path` (required): Path to config file
-
-**Example:**
-```
-Validate configs/godot.json
-```
-
-### 8. `split_config`
-Split large documentation config into multiple focused skills. For 10K+ page documentation.
-
-**Parameters:**
-- `config_path` (required): Path to config JSON file (e.g., "configs/godot.json")
-- `strategy` (optional): Split strategy - "auto", "none", "category", "router", "size" (default: "auto")
-- `target_pages` (optional): Target pages per skill (default: 5000)
-- `dry_run` (optional): Preview without saving files (default: false)
-
-**Example:**
-```
-Split configs/godot.json using router strategy with 5000 pages per skill
-```
-
-**Strategies:**
-- **auto** - Intelligently detects best strategy based on page count and config
-- **category** - Split by documentation categories (creates focused sub-skills)
-- **router** - Create router/hub skill + specialized sub-skills (RECOMMENDED for 10K+ pages)
-- **size** - Split every N pages (for docs without clear categories)
-
-### 9. `generate_router`
-Generate router/hub skill for split documentation. Creates intelligent routing to sub-skills.
-
-**Parameters:**
-- `config_pattern` (required): Config pattern for sub-skills (e.g., "configs/godot-*.json")
-- `router_name` (optional): Router skill name (inferred from configs if not provided)
-
-**Example:**
-```
-Generate router for configs/godot-*.json
-```
-
-**What it does:**
-- Analyzes all sub-skill configs
-- Extracts routing keywords from categories and names
-- Creates router SKILL.md with intelligent routing logic
-- Users can ask questions naturally, router directs to appropriate sub-skill
-
-### 10. `scrape_pdf`
-Scrape PDF documentation and build Claude skill. Extracts text, code blocks, images, and tables from PDF files with advanced features.
-
-**Parameters:**
-- `config_path` (optional): Path to PDF config JSON file (e.g., "configs/manual_pdf.json")
-- `pdf_path` (optional): Direct PDF path (alternative to config_path)
-- `name` (optional): Skill name (required with pdf_path)
-- `description` (optional): Skill description
-- `from_json` (optional): Build from extracted JSON file (e.g., "output/manual_extracted.json")
-- `use_ocr` (optional): Use OCR for scanned PDFs (requires pytesseract)
-- `password` (optional): Password for encrypted PDFs
-- `extract_tables` (optional): Extract tables from PDF
-- `parallel` (optional): Process pages in parallel for faster extraction
-- `max_workers` (optional): Number of parallel workers (default: CPU count)
-
-**Examples:**
-```
-Scrape PDF at docs/manual.pdf and create skill named api-docs
-Create skill from configs/example_pdf.json
-Build skill from output/manual_extracted.json
-Scrape scanned PDF with OCR: --pdf docs/scanned.pdf --ocr
-Scrape encrypted PDF: --pdf docs/manual.pdf --password mypassword
-Extract tables: --pdf docs/data.pdf --extract-tables
-Fast parallel processing: --pdf docs/large.pdf --parallel --workers 8
-```
-
-**What it does:**
-- Extracts text and markdown from PDF pages
-- Detects code blocks using 3 methods (font, indent, pattern)
-- Detects programming language with confidence scoring (19+ languages)
-- Validates syntax and scores code quality (0-10 scale)
-- Extracts images with size filtering
-- **NEW:** Extracts tables from PDFs (Priority 2)
-- **NEW:** OCR support for scanned PDFs (Priority 2, requires pytesseract + Pillow)
-- **NEW:** Password-protected PDF support (Priority 2)
-- **NEW:** Parallel page processing for faster extraction (Priority 3)
-- **NEW:** Intelligent caching of expensive operations (Priority 3)
-- Detects chapters and creates page chunks
-- Categorizes content automatically
-- Generates complete skill structure (SKILL.md + references)
-
-**Performance:**
-- Sequential: ~30-60 seconds per 100 pages
-- Parallel (8 workers): ~10-20 seconds per 100 pages (3x faster)
-
-**See:** `docs/PDF_SCRAPER.md` for complete PDF documentation guide
-
-## Example Workflows
-
-### Generate a New Skill from Scratch
-
-```
-User: Generate config for Svelte at https://svelte.dev/docs
-
-Claude: ✅ Config created: configs/svelte.json
-
-User: Estimate pages for configs/svelte.json
-
-Claude: 📊 Estimated pages: 150
-
-User: Scrape docs using configs/svelte.json
-
-Claude: ✅ Skill created at output/svelte/
-
-User: Package skill at output/svelte/
-
-Claude: ✅ Created: output/svelte.zip
- Ready to upload to Claude!
-```
-
-### Use Existing Preset
-
-```
-User: List all available configs
-
-Claude: [Shows all configs: godot, react, vue, django, fastapi, etc.]
-
-User: Scrape docs using configs/react.json
-
-Claude: ✅ Skill created at output/react/
-
-User: Package skill at output/react/
-
-Claude: ✅ Created: output/react.zip
-```
-
-### Validate Before Scraping
-
-```
-User: Validate configs/godot.json
-
-Claude: ✅ Config is valid!
- Name: godot
- Base URL: https://docs.godotengine.org/en/stable/
- Max pages: 500
- Rate limit: 0.5s
-
-User: Scrape docs using configs/godot.json
-
-Claude: [Starts scraping...]
-```
-
-### PDF Documentation - NEW
-
-```
-User: Scrape PDF at docs/api-manual.pdf and create skill named api-docs
-
-Claude: 📄 Scraping PDF documentation...
- ✅ Extracted 120 pages
- ✅ Found 45 code blocks (Python, JavaScript, C++)
- ✅ Extracted 12 images
- ✅ Created skill at output/api-docs/
- 📦 Package with: python3 cli/package_skill.py output/api-docs/
-
-User: Package skill at output/api-docs/
-
-Claude: ✅ Created: output/api-docs.zip
- Ready to upload to Claude!
-```
-
-### Large Documentation (40K Pages)
-
-```
-User: Estimate pages for configs/godot.json
-
-Claude: 📊 Estimated pages: 40,000
- ⚠️ Large documentation detected!
- 💡 Recommend splitting into multiple skills
-
-User: Split configs/godot.json using router strategy
-
-Claude: ✅ Split complete!
- Created 5 sub-skills:
- - godot-scripting.json (5,000 pages)
- - godot-2d.json (8,000 pages)
- - godot-3d.json (10,000 pages)
- - godot-physics.json (6,000 pages)
- - godot-shaders.json (11,000 pages)
-
-User: Scrape all godot sub-skills in parallel
-
-Claude: [Starts scraping all 5 configs in parallel...]
- ✅ All skills created in 4-8 hours instead of 20-40!
-
-User: Generate router for configs/godot-*.json
-
-Claude: ✅ Router skill created at output/godot/
- Routing logic:
- - "scripting", "gdscript" → godot-scripting
- - "2d", "sprites", "tilemap" → godot-2d
- - "3d", "meshes", "camera" → godot-3d
- - "physics", "collision" → godot-physics
- - "shaders", "visual shader" → godot-shaders
-
-User: Package all godot skills
-
-Claude: ✅ 6 skills packaged:
- - godot.zip (router)
- - godot-scripting.zip
- - godot-2d.zip
- - godot-3d.zip
- - godot-physics.zip
- - godot-shaders.zip
-
- Upload all to Claude!
- Users just ask questions naturally - router handles routing!
-```
-
-## Architecture
-
-### Server Structure
-
-```
-mcp/
-├── server.py # Main MCP server
-├── requirements.txt # MCP dependencies
-└── README.md # This file
-```
-
-### How It Works
-
-1. **Claude Code** sends MCP requests to the server
-2. **Server** routes requests to appropriate tool functions
-3. **Tools** call CLI scripts (`doc_scraper.py`, `estimate_pages.py`, etc.)
-4. **CLI scripts** perform actual work (scraping, packaging, etc.)
-5. **Results** returned to Claude Code via MCP protocol
-
-### Tool Implementation
-
-Each tool is implemented as an async function:
-
-```python
-async def generate_config_tool(args: dict) -> list[TextContent]:
- """Generate a config file"""
- # Create config JSON
- # Save to configs/
- # Return success message
-```
-
-Tools use `subprocess.run()` to call CLI scripts:
-
-```python
-result = subprocess.run([
- sys.executable,
- str(CLI_DIR / "doc_scraper.py"),
- "--config", config_path
-], capture_output=True, text=True)
-```
-
-## Testing
-
-The MCP server has comprehensive test coverage:
-
-```bash
-# Run MCP server tests (25 tests)
-python3 -m pytest tests/test_mcp_server.py -v
-
-# Expected output: 25 passed in ~0.3s
-```
-
-### Test Coverage
-
-- **Server initialization** (2 tests)
-- **Tool listing** (2 tests)
-- **generate_config** (3 tests)
-- **estimate_pages** (3 tests)
-- **scrape_docs** (4 tests)
-- **package_skill** (3 tests)
-- **upload_skill** (2 tests)
-- **list_configs** (3 tests)
-- **validate_config** (3 tests)
-- **split_config** (3 tests)
-- **generate_router** (3 tests)
-- **Tool routing** (2 tests)
-- **Integration** (1 test)
-
-**Total: 34 tests | Pass rate: 100%**
-
-## Troubleshooting
-
-### MCP Server Not Loading
-
-**Symptoms:**
-- Tools don't appear in Claude Code
-- No response to skill-seeker commands
-
-**Solutions:**
-
-1. Check configuration:
- ```bash
- cat ~/.config/claude-code/mcp.json
- ```
-
-2. Verify server can start:
- ```bash
- python3 mcp/server.py
- # Should start without errors (Ctrl+C to exit)
- ```
-
-3. Check dependencies:
- ```bash
- pip3 install -r mcp/requirements.txt
- ```
-
-4. Completely restart Claude Code (quit and reopen)
-
-5. Check Claude Code logs:
- - macOS: `~/Library/Logs/Claude Code/`
- - Linux: `~/.config/claude-code/logs/`
-
-### "ModuleNotFoundError: No module named 'mcp'"
-
-```bash
-pip3 install -r mcp/requirements.txt
-```
-
-### Tools Appear But Don't Work
-
-**Solutions:**
-
-1. Verify `cwd` in config points to repository root
-2. Check CLI tools exist:
- ```bash
- ls cli/doc_scraper.py
- ls cli/estimate_pages.py
- ls cli/package_skill.py
- ```
-
-3. Test CLI tools directly:
- ```bash
- python3 cli/doc_scraper.py --help
- ```
-
-### Slow Operations
-
-1. Check rate limit in configs (increase if needed)
-2. Use smaller `max_pages` for testing
-3. Use `skip_scrape` to avoid re-downloading data
-
-## Advanced Configuration
-
-### Using Virtual Environment
-
-```bash
-# Create venv
-python3 -m venv venv
-source venv/bin/activate
-pip install -r mcp/requirements.txt
-pip install requests beautifulsoup4
-which python3 # Copy this path
-```
-
-Configure Claude Code to use venv Python:
-
-```json
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "/path/to/Skill_Seekers/venv/bin/python3",
- "args": ["/path/to/Skill_Seekers/mcp/server.py"],
- "cwd": "/path/to/Skill_Seekers"
- }
- }
-}
-```
-
-### Debug Mode
-
-Enable verbose logging:
-
-```json
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "python3",
- "args": ["-u", "/path/to/Skill_Seekers/mcp/server.py"],
- "cwd": "/path/to/Skill_Seekers",
- "env": {
- "DEBUG": "1"
- }
- }
- }
-}
-```
-
-### With API Enhancement
-
-For API-based enhancement (requires Anthropic API key):
-
-```json
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "python3",
- "args": ["/path/to/Skill_Seekers/mcp/server.py"],
- "cwd": "/path/to/Skill_Seekers",
- "env": {
- "ANTHROPIC_API_KEY": "sk-ant-your-key-here"
- }
- }
- }
-}
-```
-
-## Performance
-
-| Operation | Time | Notes |
-|-----------|------|-------|
-| List configs | <1s | Instant |
-| Generate config | <1s | Creates JSON file |
-| Validate config | <1s | Quick validation |
-| Estimate pages | 1-2min | Fast, no data download |
-| Split config | 1-3min | Analyzes and creates sub-configs |
-| Generate router | 10-30s | Creates router SKILL.md |
-| Scrape docs | 15-45min | First time only |
-| Scrape docs (40K pages) | 20-40hrs | Sequential |
-| Scrape docs (40K pages, parallel) | 4-8hrs | 5 skills in parallel |
-| Scrape (cached) | <1min | With `skip_scrape` |
-| Package skill | 5-10s | Creates .zip |
-| Package multi | 30-60s | Packages 5-10 skills |
-
-## Documentation
-
-- **Full Setup Guide**: [docs/MCP_SETUP.md](../docs/MCP_SETUP.md)
-- **Main README**: [README.md](../README.md)
-- **Usage Guide**: [docs/USAGE.md](../docs/USAGE.md)
-- **Testing Guide**: [docs/TESTING.md](../docs/TESTING.md)
-
-## Support
-
-- **Issues**: [GitHub Issues](https://github.com/yusufkaraaslan/Skill_Seekers/issues)
-- **Discussions**: [GitHub Discussions](https://github.com/yusufkaraaslan/Skill_Seekers/discussions)
-
-## License
-
-MIT License - See [LICENSE](../LICENSE) for details
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/__init__.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/__init__.py
deleted file mode 100644
index 4616b37..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Skill Seekers MCP (Model Context Protocol) server package.
-
-This package provides MCP server integration for Claude Code, allowing
-natural language interaction with Skill Seekers tools.
-
-Main modules:
- - server: MCP server implementation with 9 tools
-
-Available MCP Tools:
- - list_configs: List all available preset configurations
- - generate_config: Generate a new config file for any docs site
- - validate_config: Validate a config file structure
- - estimate_pages: Estimate page count before scraping
- - scrape_docs: Scrape and build a skill
- - package_skill: Package skill into .zip file (with auto-upload)
- - upload_skill: Upload .zip to Claude
- - split_config: Split large documentation configs
- - generate_router: Generate router/hub skills
-
-Usage:
- The MCP server is typically run by Claude Code via configuration
- in ~/.config/claude-code/mcp.json
-"""
-
-__version__ = "2.0.0"
-
-__all__ = []
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/requirements.txt b/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/requirements.txt
deleted file mode 100644
index 18088ef..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# MCP Server dependencies
-mcp>=1.0.0
-
-# CLI tool dependencies (shared)
-requests>=2.31.0
-beautifulsoup4>=4.12.0
-
-# Optional: for API-based enhancement
-# anthropic>=0.18.0
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/server.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/server.py
deleted file mode 100644
index 4e054de..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/server.py
+++ /dev/null
@@ -1,1064 +0,0 @@
-#!/usr/bin/env python3
-"""
-Skill Seeker MCP Server
-Model Context Protocol server for generating Claude AI skills from documentation
-"""
-
-import asyncio
-import json
-import os
-import subprocess
-import sys
-import time
-from pathlib import Path
-from typing import Any
-
-# Import external MCP package
-# NOTE: Directory renamed from 'mcp/' to 'skill_seeker_mcp/' to avoid shadowing the external mcp package
-MCP_AVAILABLE = False
-Server = None
-Tool = None
-TextContent = None
-
-try:
- from mcp.server import Server
- from mcp.types import Tool, TextContent
- MCP_AVAILABLE = True
-except ImportError as e:
- if __name__ == "__main__":
- print("❌ Error: mcp package not installed")
- print("Install with: pip install mcp")
- print(f"Import error: {e}")
- sys.exit(1)
-
-
-# Initialize MCP server (only if MCP is available)
-app = Server("skill-seeker") if MCP_AVAILABLE and Server is not None else None
-
-# Path to CLI tools
-CLI_DIR = Path(__file__).parent.parent / "cli"
-
-# Helper decorator that works even when app is None
-def safe_decorator(decorator_func):
- """Returns the decorator if MCP is available, otherwise returns a no-op"""
- if MCP_AVAILABLE and app is not None:
- return decorator_func
- else:
- # Return a decorator that just returns the function unchanged
- def noop_decorator(func):
- return func
- return noop_decorator
-
-
-def run_subprocess_with_streaming(cmd, timeout=None):
- """
- Run subprocess with real-time output streaming.
- Returns (stdout, stderr, returncode).
-
- This solves the blocking issue where long-running processes (like scraping)
- would cause MCP to appear frozen. Now we stream output as it comes.
- """
- try:
- process = subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- bufsize=1, # Line buffered
- universal_newlines=True
- )
-
- stdout_lines = []
- stderr_lines = []
- start_time = time.time()
-
- # Read output line by line as it comes
- while True:
- # Check timeout
- if timeout and (time.time() - start_time) > timeout:
- process.kill()
- stderr_lines.append(f"\n⚠️ Process killed after {timeout}s timeout")
- break
-
- # Check if process finished
- if process.poll() is not None:
- break
-
- # Read available output (non-blocking)
- try:
- import select
- readable, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
-
- if process.stdout in readable:
- line = process.stdout.readline()
- if line:
- stdout_lines.append(line)
-
- if process.stderr in readable:
- line = process.stderr.readline()
- if line:
- stderr_lines.append(line)
- except:
- # Fallback for Windows (no select)
- time.sleep(0.1)
-
- # Get any remaining output
- remaining_stdout, remaining_stderr = process.communicate()
- if remaining_stdout:
- stdout_lines.append(remaining_stdout)
- if remaining_stderr:
- stderr_lines.append(remaining_stderr)
-
- stdout = ''.join(stdout_lines)
- stderr = ''.join(stderr_lines)
- returncode = process.returncode
-
- return stdout, stderr, returncode
-
- except Exception as e:
- return "", f"Error running subprocess: {str(e)}", 1
-
-
-@safe_decorator(app.list_tools() if app else lambda: lambda f: f)
-async def list_tools() -> list[Tool]:
- """List available tools"""
- return [
- Tool(
- name="generate_config",
- description="Generate a config file for documentation scraping. Interactively creates a JSON config for any documentation website.",
- inputSchema={
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Skill name (lowercase, alphanumeric, hyphens, underscores)",
- },
- "url": {
- "type": "string",
- "description": "Base documentation URL (must include http:// or https://)",
- },
- "description": {
- "type": "string",
- "description": "Description of when to use this skill",
- },
- "max_pages": {
- "type": "integer",
- "description": "Maximum pages to scrape (default: 100, use -1 for unlimited)",
- "default": 100,
- },
- "unlimited": {
- "type": "boolean",
- "description": "Remove all limits - scrape all pages (default: false). Overrides max_pages.",
- "default": False,
- },
- "rate_limit": {
- "type": "number",
- "description": "Delay between requests in seconds (default: 0.5)",
- "default": 0.5,
- },
- },
- "required": ["name", "url", "description"],
- },
- ),
- Tool(
- name="estimate_pages",
- description="Estimate how many pages will be scraped from a config. Fast preview without downloading content.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to config JSON file (e.g., configs/react.json)",
- },
- "max_discovery": {
- "type": "integer",
- "description": "Maximum pages to discover during estimation (default: 1000, use -1 for unlimited)",
- "default": 1000,
- },
- "unlimited": {
- "type": "boolean",
- "description": "Remove discovery limit - estimate all pages (default: false). Overrides max_discovery.",
- "default": False,
- },
- },
- "required": ["config_path"],
- },
- ),
- Tool(
- name="scrape_docs",
- description="Scrape documentation and build Claude skill. Supports both single-source (legacy) and unified multi-source configs. Creates SKILL.md and reference files. Automatically detects llms.txt files for 10x faster processing. Falls back to HTML scraping if not available.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to config JSON file (e.g., configs/react.json or configs/godot_unified.json)",
- },
- "unlimited": {
- "type": "boolean",
- "description": "Remove page limit - scrape all pages (default: false). Overrides max_pages in config.",
- "default": False,
- },
- "enhance_local": {
- "type": "boolean",
- "description": "Open terminal for local enhancement with Claude Code (default: false)",
- "default": False,
- },
- "skip_scrape": {
- "type": "boolean",
- "description": "Skip scraping, use cached data (default: false)",
- "default": False,
- },
- "dry_run": {
- "type": "boolean",
- "description": "Preview what will be scraped without saving (default: false)",
- "default": False,
- },
- "merge_mode": {
- "type": "string",
- "description": "Override merge mode for unified configs: 'rule-based' or 'claude-enhanced' (default: from config)",
- },
- },
- "required": ["config_path"],
- },
- ),
- Tool(
- name="package_skill",
- description="Package a skill directory into a .zip file ready for Claude upload. Automatically uploads if ANTHROPIC_API_KEY is set.",
- inputSchema={
- "type": "object",
- "properties": {
- "skill_dir": {
- "type": "string",
- "description": "Path to skill directory (e.g., output/react/)",
- },
- "auto_upload": {
- "type": "boolean",
- "description": "Try to upload automatically if API key is available (default: true). If false, only package without upload attempt.",
- "default": True,
- },
- },
- "required": ["skill_dir"],
- },
- ),
- Tool(
- name="upload_skill",
- description="Upload a skill .zip file to Claude automatically (requires ANTHROPIC_API_KEY)",
- inputSchema={
- "type": "object",
- "properties": {
- "skill_zip": {
- "type": "string",
- "description": "Path to skill .zip file (e.g., output/react.zip)",
- },
- },
- "required": ["skill_zip"],
- },
- ),
- Tool(
- name="list_configs",
- description="List all available preset configurations.",
- inputSchema={
- "type": "object",
- "properties": {},
- },
- ),
- Tool(
- name="validate_config",
- description="Validate a config file for errors.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to config JSON file",
- },
- },
- "required": ["config_path"],
- },
- ),
- Tool(
- name="split_config",
- description="Split large documentation config into multiple focused skills. For 10K+ page documentation.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to config JSON file (e.g., configs/godot.json)",
- },
- "strategy": {
- "type": "string",
- "description": "Split strategy: auto, none, category, router, size (default: auto)",
- "default": "auto",
- },
- "target_pages": {
- "type": "integer",
- "description": "Target pages per skill (default: 5000)",
- "default": 5000,
- },
- "dry_run": {
- "type": "boolean",
- "description": "Preview without saving files (default: false)",
- "default": False,
- },
- },
- "required": ["config_path"],
- },
- ),
- Tool(
- name="generate_router",
- description="Generate router/hub skill for split documentation. Creates intelligent routing to sub-skills.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_pattern": {
- "type": "string",
- "description": "Config pattern for sub-skills (e.g., 'configs/godot-*.json')",
- },
- "router_name": {
- "type": "string",
- "description": "Router skill name (optional, inferred from configs)",
- },
- },
- "required": ["config_pattern"],
- },
- ),
- Tool(
- name="scrape_pdf",
- description="Scrape PDF documentation and build Claude skill. Extracts text, code, and images from PDF files.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to PDF config JSON file (e.g., configs/manual_pdf.json)",
- },
- "pdf_path": {
- "type": "string",
- "description": "Direct PDF path (alternative to config_path)",
- },
- "name": {
- "type": "string",
- "description": "Skill name (required with pdf_path)",
- },
- "description": {
- "type": "string",
- "description": "Skill description (optional)",
- },
- "from_json": {
- "type": "string",
- "description": "Build from extracted JSON file (e.g., output/manual_extracted.json)",
- },
- },
- "required": [],
- },
- ),
- Tool(
- name="scrape_github",
- description="Scrape GitHub repository and build Claude skill. Extracts README, Issues, Changelog, Releases, and code structure.",
- inputSchema={
- "type": "object",
- "properties": {
- "repo": {
- "type": "string",
- "description": "GitHub repository (owner/repo, e.g., facebook/react)",
- },
- "config_path": {
- "type": "string",
- "description": "Path to GitHub config JSON file (e.g., configs/react_github.json)",
- },
- "name": {
- "type": "string",
- "description": "Skill name (default: repo name)",
- },
- "description": {
- "type": "string",
- "description": "Skill description",
- },
- "token": {
- "type": "string",
- "description": "GitHub personal access token (or use GITHUB_TOKEN env var)",
- },
- "no_issues": {
- "type": "boolean",
- "description": "Skip GitHub issues extraction (default: false)",
- "default": False,
- },
- "no_changelog": {
- "type": "boolean",
- "description": "Skip CHANGELOG extraction (default: false)",
- "default": False,
- },
- "no_releases": {
- "type": "boolean",
- "description": "Skip releases extraction (default: false)",
- "default": False,
- },
- "max_issues": {
- "type": "integer",
- "description": "Maximum issues to fetch (default: 100)",
- "default": 100,
- },
- "scrape_only": {
- "type": "boolean",
- "description": "Only scrape, don't build skill (default: false)",
- "default": False,
- },
- },
- "required": [],
- },
- ),
- ]
-
-
-@safe_decorator(app.call_tool() if app else lambda: lambda f: f)
-async def call_tool(name: str, arguments: Any) -> list[TextContent]:
- """Handle tool calls"""
-
- try:
- if name == "generate_config":
- return await generate_config_tool(arguments)
- elif name == "estimate_pages":
- return await estimate_pages_tool(arguments)
- elif name == "scrape_docs":
- return await scrape_docs_tool(arguments)
- elif name == "package_skill":
- return await package_skill_tool(arguments)
- elif name == "upload_skill":
- return await upload_skill_tool(arguments)
- elif name == "list_configs":
- return await list_configs_tool(arguments)
- elif name == "validate_config":
- return await validate_config_tool(arguments)
- elif name == "split_config":
- return await split_config_tool(arguments)
- elif name == "generate_router":
- return await generate_router_tool(arguments)
- elif name == "scrape_pdf":
- return await scrape_pdf_tool(arguments)
- elif name == "scrape_github":
- return await scrape_github_tool(arguments)
- else:
- return [TextContent(type="text", text=f"Unknown tool: {name}")]
-
- except Exception as e:
- return [TextContent(type="text", text=f"Error: {str(e)}")]
-
-
-async def generate_config_tool(args: dict) -> list[TextContent]:
- """Generate a config file"""
- name = args["name"]
- url = args["url"]
- description = args["description"]
- max_pages = args.get("max_pages", 100)
- unlimited = args.get("unlimited", False)
- rate_limit = args.get("rate_limit", 0.5)
-
- # Handle unlimited mode
- if unlimited:
- max_pages = None
- limit_msg = "unlimited (no page limit)"
- elif max_pages == -1:
- max_pages = None
- limit_msg = "unlimited (no page limit)"
- else:
- limit_msg = str(max_pages)
-
- # Create config
- config = {
- "name": name,
- "description": description,
- "base_url": url,
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [],
- "exclude": []
- },
- "categories": {},
- "rate_limit": rate_limit,
- "max_pages": max_pages
- }
-
- # Save to configs directory
- config_path = Path("configs") / f"{name}.json"
- config_path.parent.mkdir(exist_ok=True)
-
- with open(config_path, 'w') as f:
- json.dump(config, f, indent=2)
-
- result = f"""✅ Config created: {config_path}
-
-Configuration:
- Name: {name}
- URL: {url}
- Max pages: {limit_msg}
- Rate limit: {rate_limit}s
-
-Next steps:
- 1. Review/edit config: cat {config_path}
- 2. Estimate pages: Use estimate_pages tool
- 3. Scrape docs: Use scrape_docs tool
-
-Note: Default selectors may need adjustment for your documentation site.
-"""
-
- return [TextContent(type="text", text=result)]
-
-
-async def estimate_pages_tool(args: dict) -> list[TextContent]:
- """Estimate page count"""
- config_path = args["config_path"]
- max_discovery = args.get("max_discovery", 1000)
- unlimited = args.get("unlimited", False)
-
- # Handle unlimited mode
- if unlimited or max_discovery == -1:
- max_discovery = -1
- timeout = 1800 # 30 minutes for unlimited discovery
- else:
- # Estimate: 0.5s per page discovered
- timeout = max(300, max_discovery // 2) # Minimum 5 minutes
-
- # Run estimate_pages.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "estimate_pages.py"),
- config_path,
- "--max-discovery", str(max_discovery)
- ]
-
- progress_msg = f"🔄 Estimating page count...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def scrape_docs_tool(args: dict) -> list[TextContent]:
- """Scrape documentation - auto-detects unified vs legacy format"""
- config_path = args["config_path"]
- unlimited = args.get("unlimited", False)
- enhance_local = args.get("enhance_local", False)
- skip_scrape = args.get("skip_scrape", False)
- dry_run = args.get("dry_run", False)
- merge_mode = args.get("merge_mode")
-
- # Load config to detect format
- with open(config_path, 'r') as f:
- config = json.load(f)
-
- # Detect if unified format (has 'sources' array)
- is_unified = 'sources' in config and isinstance(config['sources'], list)
-
- # Handle unlimited mode by modifying config temporarily
- if unlimited:
- # Set max_pages to None (unlimited)
- if is_unified:
- # For unified configs, set max_pages on documentation sources
- for source in config.get('sources', []):
- if source.get('type') == 'documentation':
- source['max_pages'] = None
- else:
- # For legacy configs
- config['max_pages'] = None
-
- # Create temporary config file
- temp_config_path = config_path.replace('.json', '_unlimited_temp.json')
- with open(temp_config_path, 'w') as f:
- json.dump(config, f, indent=2)
-
- config_to_use = temp_config_path
- else:
- config_to_use = config_path
-
- # Choose scraper based on format
- if is_unified:
- scraper_script = "unified_scraper.py"
- progress_msg = f"🔄 Starting unified multi-source scraping...\n"
- progress_msg += f"📦 Config format: Unified (multiple sources)\n"
- else:
- scraper_script = "doc_scraper.py"
- progress_msg = f"🔄 Starting scraping process...\n"
- progress_msg += f"📦 Config format: Legacy (single source)\n"
-
- # Build command
- cmd = [
- sys.executable,
- str(CLI_DIR / scraper_script),
- "--config", config_to_use
- ]
-
- # Add merge mode for unified configs
- if is_unified and merge_mode:
- cmd.extend(["--merge-mode", merge_mode])
-
- # Add --fresh to avoid user input prompts when existing data found
- if not skip_scrape:
- cmd.append("--fresh")
-
- if enhance_local:
- cmd.append("--enhance-local")
- if skip_scrape:
- cmd.append("--skip-scrape")
- if dry_run:
- cmd.append("--dry-run")
-
- # Determine timeout based on operation type
- if dry_run:
- timeout = 300 # 5 minutes for dry run
- elif skip_scrape:
- timeout = 600 # 10 minutes for building from cache
- elif unlimited:
- timeout = None # No timeout for unlimited mode (user explicitly requested)
- else:
- # Read config to estimate timeout
- try:
- if is_unified:
- # For unified configs, estimate based on all sources
- total_pages = 0
- for source in config.get('sources', []):
- if source.get('type') == 'documentation':
- total_pages += source.get('max_pages', 500)
- max_pages = total_pages or 500
- else:
- max_pages = config.get('max_pages', 500)
-
- # Estimate: 30s per page + buffer
- timeout = max(3600, max_pages * 35) # Minimum 1 hour, or 35s per page
- except:
- timeout = 14400 # Default: 4 hours
-
- # Add progress message
- if timeout:
- progress_msg += f"⏱️ Maximum time allowed: {timeout // 60} minutes\n"
- else:
- progress_msg += f"⏱️ Unlimited mode - no timeout\n"
- progress_msg += f"📝 Progress will be shown below:\n\n"
-
- # Run scraper with streaming
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- # Clean up temporary config
- if unlimited and Path(config_to_use).exists():
- Path(config_to_use).unlink()
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- error_output = output + f"\n\n❌ Error:\n{stderr}"
- return [TextContent(type="text", text=error_output)]
-
-
-async def package_skill_tool(args: dict) -> list[TextContent]:
- """Package skill to .zip and optionally auto-upload"""
- skill_dir = args["skill_dir"]
- auto_upload = args.get("auto_upload", True)
-
- # Check if API key exists - only upload if available
- has_api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
- should_upload = auto_upload and has_api_key
-
- # Run package_skill.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "package_skill.py"),
- skill_dir,
- "--no-open", # Don't open folder in MCP context
- "--skip-quality-check" # Skip interactive quality checks in MCP context
- ]
-
- # Add upload flag only if we have API key
- if should_upload:
- cmd.append("--upload")
-
- # Timeout: 5 minutes for packaging + upload
- timeout = 300
-
- progress_msg = "📦 Packaging skill...\n"
- if should_upload:
- progress_msg += "📤 Will auto-upload if successful\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- if should_upload:
- # Upload succeeded
- output += "\n\n✅ Skill packaged and uploaded automatically!"
- output += "\n Your skill is now available in Claude!"
- elif auto_upload and not has_api_key:
- # User wanted upload but no API key
- output += "\n\n📝 Skill packaged successfully!"
- output += "\n"
- output += "\n💡 To enable automatic upload:"
- output += "\n 1. Get API key from https://console.anthropic.com/"
- output += "\n 2. Set: export ANTHROPIC_API_KEY=sk-ant-..."
- output += "\n"
- output += "\n📤 Manual upload:"
- output += "\n 1. Find the .zip file in your output/ folder"
- output += "\n 2. Go to https://claude.ai/skills"
- output += "\n 3. Click 'Upload Skill' and select the .zip file"
- else:
- # auto_upload=False, just packaged
- output += "\n\n✅ Skill packaged successfully!"
- output += "\n Upload manually to https://claude.ai/skills"
-
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def upload_skill_tool(args: dict) -> list[TextContent]:
- """Upload skill .zip to Claude"""
- skill_zip = args["skill_zip"]
-
- # Run upload_skill.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "upload_skill.py"),
- skill_zip
- ]
-
- # Timeout: 5 minutes for upload
- timeout = 300
-
- progress_msg = "📤 Uploading skill to Claude...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def list_configs_tool(args: dict) -> list[TextContent]:
- """List available configs"""
- configs_dir = Path("configs")
-
- if not configs_dir.exists():
- return [TextContent(type="text", text="No configs directory found")]
-
- configs = list(configs_dir.glob("*.json"))
-
- if not configs:
- return [TextContent(type="text", text="No config files found")]
-
- result = "📋 Available Configs:\n\n"
-
- for config_file in sorted(configs):
- try:
- with open(config_file) as f:
- config = json.load(f)
- name = config.get("name", config_file.stem)
- desc = config.get("description", "No description")
- url = config.get("base_url", "")
-
- result += f" • {config_file.name}\n"
- result += f" Name: {name}\n"
- result += f" URL: {url}\n"
- result += f" Description: {desc}\n\n"
- except Exception as e:
- result += f" • {config_file.name} - Error reading: {e}\n\n"
-
- return [TextContent(type="text", text=result)]
-
-
-async def validate_config_tool(args: dict) -> list[TextContent]:
- """Validate a config file - supports both legacy and unified formats"""
- config_path = args["config_path"]
-
- # Import validation classes
- sys.path.insert(0, str(CLI_DIR))
-
- try:
- # Check if file exists
- if not Path(config_path).exists():
- return [TextContent(type="text", text=f"❌ Error: Config file not found: {config_path}")]
-
- # Try unified config validator first
- try:
- from config_validator import validate_config
- validator = validate_config(config_path)
-
- result = f"✅ Config is valid!\n\n"
-
- # Show format
- if validator.is_unified:
- result += f"📦 Format: Unified (multi-source)\n"
- result += f" Name: {validator.config['name']}\n"
- result += f" Sources: {len(validator.config.get('sources', []))}\n"
-
- # Show sources
- for i, source in enumerate(validator.config.get('sources', []), 1):
- result += f"\n Source {i}: {source['type']}\n"
- if source['type'] == 'documentation':
- result += f" URL: {source.get('base_url', 'N/A')}\n"
- result += f" Max pages: {source.get('max_pages', 'Not set')}\n"
- elif source['type'] == 'github':
- result += f" Repo: {source.get('repo', 'N/A')}\n"
- result += f" Code depth: {source.get('code_analysis_depth', 'surface')}\n"
- elif source['type'] == 'pdf':
- result += f" Path: {source.get('path', 'N/A')}\n"
-
- # Show merge settings if applicable
- if validator.needs_api_merge():
- merge_mode = validator.config.get('merge_mode', 'rule-based')
- result += f"\n Merge mode: {merge_mode}\n"
- result += f" API merging: Required (docs + code sources)\n"
-
- else:
- result += f"📦 Format: Legacy (single source)\n"
- result += f" Name: {validator.config['name']}\n"
- result += f" Base URL: {validator.config.get('base_url', 'N/A')}\n"
- result += f" Max pages: {validator.config.get('max_pages', 'Not set')}\n"
- result += f" Rate limit: {validator.config.get('rate_limit', 'Not set')}s\n"
-
- return [TextContent(type="text", text=result)]
-
- except ImportError:
- # Fall back to legacy validation
- from doc_scraper import validate_config
- import json
-
- with open(config_path, 'r') as f:
- config = json.load(f)
-
- # Validate config - returns (errors, warnings) tuple
- errors, warnings = validate_config(config)
-
- if errors:
- result = f"❌ Config validation failed:\n\n"
- for error in errors:
- result += f" • {error}\n"
- else:
- result = f"✅ Config is valid!\n\n"
- result += f"📦 Format: Legacy (single source)\n"
- result += f" Name: {config['name']}\n"
- result += f" Base URL: {config['base_url']}\n"
- result += f" Max pages: {config.get('max_pages', 'Not set')}\n"
- result += f" Rate limit: {config.get('rate_limit', 'Not set')}s\n"
-
- if warnings:
- result += f"\n⚠️ Warnings:\n"
- for warning in warnings:
- result += f" • {warning}\n"
-
- return [TextContent(type="text", text=result)]
-
- except Exception as e:
- return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
-
-
-async def split_config_tool(args: dict) -> list[TextContent]:
- """Split large config into multiple focused configs"""
- config_path = args["config_path"]
- strategy = args.get("strategy", "auto")
- target_pages = args.get("target_pages", 5000)
- dry_run = args.get("dry_run", False)
-
- # Run split_config.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "split_config.py"),
- config_path,
- "--strategy", strategy,
- "--target-pages", str(target_pages)
- ]
-
- if dry_run:
- cmd.append("--dry-run")
-
- # Timeout: 5 minutes for config splitting
- timeout = 300
-
- progress_msg = "✂️ Splitting configuration...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def generate_router_tool(args: dict) -> list[TextContent]:
- """Generate router skill for split documentation"""
- import glob
-
- config_pattern = args["config_pattern"]
- router_name = args.get("router_name")
-
- # Expand glob pattern
- config_files = glob.glob(config_pattern)
-
- if not config_files:
- return [TextContent(type="text", text=f"❌ No config files match pattern: {config_pattern}")]
-
- # Run generate_router.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "generate_router.py"),
- ] + config_files
-
- if router_name:
- cmd.extend(["--name", router_name])
-
- # Timeout: 5 minutes for router generation
- timeout = 300
-
- progress_msg = "🧭 Generating router skill...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def scrape_pdf_tool(args: dict) -> list[TextContent]:
- """Scrape PDF documentation and build skill"""
- config_path = args.get("config_path")
- pdf_path = args.get("pdf_path")
- name = args.get("name")
- description = args.get("description")
- from_json = args.get("from_json")
-
- # Build command
- cmd = [sys.executable, str(CLI_DIR / "pdf_scraper.py")]
-
- # Mode 1: Config file
- if config_path:
- cmd.extend(["--config", config_path])
-
- # Mode 2: Direct PDF
- elif pdf_path and name:
- cmd.extend(["--pdf", pdf_path, "--name", name])
- if description:
- cmd.extend(["--description", description])
-
- # Mode 3: From JSON
- elif from_json:
- cmd.extend(["--from-json", from_json])
-
- else:
- return [TextContent(type="text", text="❌ Error: Must specify --config, --pdf + --name, or --from-json")]
-
- # Run pdf_scraper.py with streaming (can take a while)
- timeout = 600 # 10 minutes for PDF extraction
-
- progress_msg = "📄 Scraping PDF documentation...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def scrape_github_tool(args: dict) -> list[TextContent]:
- """Scrape GitHub repository to Claude skill (C1.11)"""
- repo = args.get("repo")
- config_path = args.get("config_path")
- name = args.get("name")
- description = args.get("description")
- token = args.get("token")
- no_issues = args.get("no_issues", False)
- no_changelog = args.get("no_changelog", False)
- no_releases = args.get("no_releases", False)
- max_issues = args.get("max_issues", 100)
- scrape_only = args.get("scrape_only", False)
-
- # Build command
- cmd = [sys.executable, str(CLI_DIR / "github_scraper.py")]
-
- # Mode 1: Config file
- if config_path:
- cmd.extend(["--config", config_path])
-
- # Mode 2: Direct repo
- elif repo:
- cmd.extend(["--repo", repo])
- if name:
- cmd.extend(["--name", name])
- if description:
- cmd.extend(["--description", description])
- if token:
- cmd.extend(["--token", token])
- if no_issues:
- cmd.append("--no-issues")
- if no_changelog:
- cmd.append("--no-changelog")
- if no_releases:
- cmd.append("--no-releases")
- if max_issues != 100:
- cmd.extend(["--max-issues", str(max_issues)])
- if scrape_only:
- cmd.append("--scrape-only")
-
- else:
- return [TextContent(type="text", text="❌ Error: Must specify --repo or --config")]
-
- # Run github_scraper.py with streaming (can take a while)
- timeout = 600 # 10 minutes for GitHub scraping
-
- progress_msg = "🐙 Scraping GitHub repository...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def main():
- """Run the MCP server"""
- if not MCP_AVAILABLE or app is None:
- print("❌ Error: MCP server cannot start - MCP package not available")
- sys.exit(1)
-
- from mcp.server.stdio import stdio_server
-
- async with stdio_server() as (read_stream, write_stream):
- await app.run(
- read_stream,
- write_stream,
- app.create_initialization_options()
- )
-
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/tools/__init__.py b/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/tools/__init__.py
deleted file mode 100644
index 388f312..0000000
--- a/assets/repo/Skill_Seekers-development/src/skill_seekers/mcp/tools/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""MCP tools subpackage.
-
-This package will contain modularized MCP tool implementations.
-
-Planned structure (for future refactoring):
- - scraping_tools.py: Tools for scraping (estimate_pages, scrape_docs)
- - building_tools.py: Tools for building (package_skill, validate_config)
- - deployment_tools.py: Tools for deployment (upload_skill)
- - config_tools.py: Tools for configs (list_configs, generate_config)
- - advanced_tools.py: Advanced tools (split_config, generate_router)
-
-Current state:
- All tools are currently implemented in mcp/server.py
- This directory is a placeholder for future modularization.
-"""
-
-__version__ = "2.0.0"
-
-__all__ = []
diff --git a/assets/repo/Skill_Seekers-development/tests/__init__.py b/assets/repo/Skill_Seekers-development/tests/__init__.py
deleted file mode 100644
index ebe6270..0000000
--- a/assets/repo/Skill_Seekers-development/tests/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Test package for Skill Seeker
diff --git a/assets/repo/Skill_Seekers-development/tests/conftest.py b/assets/repo/Skill_Seekers-development/tests/conftest.py
deleted file mode 100644
index 77d483d..0000000
--- a/assets/repo/Skill_Seekers-development/tests/conftest.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-Pytest configuration for tests.
-
-Configures anyio to only use asyncio backend (not trio).
-Checks that the skill_seekers package is installed before running tests.
-"""
-
-import sys
-import pytest
-
-
-def pytest_configure(config):
- """Check if package is installed before running tests."""
- try:
- import skill_seekers
- except ModuleNotFoundError:
- print("\n" + "=" * 70)
- print("ERROR: skill_seekers package not installed")
- print("=" * 70)
- print("\nPlease install the package in editable mode first:")
- print(" pip install -e .")
- print("\nOr activate your virtual environment if you already installed it.")
- print("=" * 70 + "\n")
- sys.exit(1)
-
-
-@pytest.fixture(scope="session")
-def anyio_backend():
- """Override anyio backend to only use asyncio (not trio)."""
- return "asyncio"
diff --git a/assets/repo/Skill_Seekers-development/tests/fixtures/example_conflicts.json b/assets/repo/Skill_Seekers-development/tests/fixtures/example_conflicts.json
deleted file mode 100644
index 09a1e0e..0000000
--- a/assets/repo/Skill_Seekers-development/tests/fixtures/example_conflicts.json
+++ /dev/null
@@ -1,142 +0,0 @@
-{
- "conflicts": [
- {
- "type": "missing_in_docs",
- "severity": "medium",
- "api_name": "Node2D",
- "docs_info": null,
- "code_info": {
- "name": "Node2D",
- "type": "class",
- "source": "scene/node2d.py",
- "line": 10,
- "base_classes": [
- "Node"
- ],
- "docstring": "Base class for 2D nodes"
- },
- "difference": "API exists in code (scene/node2d.py) but not found in documentation",
- "suggestion": "Add documentation for this API"
- },
- {
- "type": "missing_in_docs",
- "severity": "medium",
- "api_name": "Node2D.move_local_x",
- "docs_info": null,
- "code_info": {
- "name": "Node2D.move_local_x",
- "type": "method",
- "parameters": [
- {
- "name": "self",
- "type_hint": null,
- "default": null
- },
- {
- "name": "delta",
- "type_hint": "float",
- "default": null
- },
- {
- "name": "snap",
- "type_hint": "bool",
- "default": "False"
- }
- ],
- "return_type": "None",
- "source": "scene/node2d.py",
- "line": 45,
- "docstring": "Move node along local X axis",
- "is_async": false
- },
- "difference": "API exists in code (scene/node2d.py) but not found in documentation",
- "suggestion": "Add documentation for this API"
- },
- {
- "type": "missing_in_docs",
- "severity": "medium",
- "api_name": "Node2D.tween_position",
- "docs_info": null,
- "code_info": {
- "name": "Node2D.tween_position",
- "type": "method",
- "parameters": [
- {
- "name": "self",
- "type_hint": null,
- "default": null
- },
- {
- "name": "target",
- "type_hint": "tuple",
- "default": null
- }
- ],
- "return_type": "None",
- "source": "scene/node2d.py",
- "line": 52,
- "docstring": "Animate to target position",
- "is_async": true
- },
- "difference": "API exists in code (scene/node2d.py) but not found in documentation",
- "suggestion": "Add documentation for this API"
- },
- {
- "type": "missing_in_code",
- "severity": "high",
- "api_name": "move_local_x",
- "docs_info": {
- "name": "move_local_x",
- "parameters": [
- {
- "name": "delta",
- "type": "float",
- "default": null
- }
- ],
- "return_type": "def",
- "source": "https://example.com/api/node2d",
- "raw_signature": "def move_local_x(delta: float)"
- },
- "code_info": null,
- "difference": "API documented (https://example.com/api/node2d) but not found in code",
- "suggestion": "Update documentation to remove this API, or add it to codebase"
- },
- {
- "type": "missing_in_code",
- "severity": "high",
- "api_name": "rotate",
- "docs_info": {
- "name": "rotate",
- "parameters": [
- {
- "name": "angle",
- "type": "float",
- "default": null
- }
- ],
- "return_type": "def",
- "source": "https://example.com/api/node2d",
- "raw_signature": "def rotate(angle: float)"
- },
- "code_info": null,
- "difference": "API documented (https://example.com/api/node2d) but not found in code",
- "suggestion": "Update documentation to remove this API, or add it to codebase"
- }
- ],
- "summary": {
- "total": 5,
- "by_type": {
- "missing_in_docs": 3,
- "missing_in_code": 2,
- "signature_mismatch": 0,
- "description_mismatch": 0
- },
- "by_severity": {
- "low": 0,
- "medium": 3,
- "high": 2
- },
- "apis_affected": 5
- }
-}
\ No newline at end of file
diff --git a/assets/repo/Skill_Seekers-development/tests/mcp_integration_test.md b/assets/repo/Skill_Seekers-development/tests/mcp_integration_test.md
deleted file mode 100644
index e04ebd5..0000000
--- a/assets/repo/Skill_Seekers-development/tests/mcp_integration_test.md
+++ /dev/null
@@ -1,567 +0,0 @@
-# MCP Integration Test Results
-
-Test documentation for Skill Seeker MCP server with Claude Code.
-
----
-
-## Test Overview
-
-**Goal:** Verify MCP server works correctly with actual Claude Code instance
-
-**Date:** [To be filled when tested]
-
-**Tester:** [To be filled]
-
-**Environment:**
-- OS: [macOS / Linux / Windows WSL]
-- Python Version: [e.g., 3.11.5]
-- Claude Code Version: [e.g., 1.0.0]
-- MCP Package Version: [e.g., 0.9.0]
-
----
-
-## Setup Checklist
-
-- [ ] Python 3.7+ installed
-- [ ] Claude Code installed and running
-- [ ] Repository cloned
-- [ ] MCP dependencies installed (`pip3 install -r mcp/requirements.txt`)
-- [ ] CLI dependencies installed (`pip3 install requests beautifulsoup4`)
-- [ ] MCP server configured in `~/.config/claude-code/mcp.json`
-- [ ] Claude Code restarted after configuration
-
----
-
-## Test Cases
-
-### Test 1: List Configs
-
-**Command:**
-```
-List all available configs
-```
-
-**Expected Result:**
-- Shows 7 preset configurations
-- Lists: godot, react, vue, django, fastapi, kubernetes, steam-economy-complete
-- Each with description
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**Status:** [ ] Pass / [ ] Fail
-
-**Notes:**
-```
-[Any observations]
-```
-
----
-
-### Test 2: Validate Config
-
-**Command:**
-```
-Validate configs/react.json
-```
-
-**Expected Result:**
-- Shows "Config is valid"
-- Displays config details (base_url, max_pages, rate_limit, categories)
-- No errors or warnings
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**Status:** [ ] Pass / [ ] Fail
-
-**Notes:**
-```
-[Any observations]
-```
-
----
-
-### Test 3: Generate Config
-
-**Command:**
-```
-Generate config for Tailwind CSS at https://tailwindcss.com/docs
-```
-
-**Expected Result:**
-- Creates `configs/tailwind.json`
-- File contains valid JSON
-- Has required fields: name, base_url, description
-- Has default values for optional fields
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**Config File Created:** [ ] Yes / [ ] No
-
-**Config Validation:**
-```bash
-# Verify file exists
-ls configs/tailwind.json
-
-# Verify valid JSON
-python3 -m json.tool configs/tailwind.json
-
-# Check contents
-cat configs/tailwind.json
-```
-
-**Status:** [ ] Pass / [ ] Fail
-
-**Notes:**
-```
-[Any observations]
-```
-
----
-
-### Test 4: Estimate Pages
-
-**Command:**
-```
-Estimate pages for configs/react.json with max discovery 100
-```
-
-**Expected Result:**
-- Shows progress during estimation
-- Completes in ~30-60 seconds
-- Shows discovered pages count
-- Shows estimated total
-- Recommends max_pages value
-- No errors or timeouts
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**Performance:**
-- Time taken: [X seconds]
-- Pages discovered: [X]
-- Estimated total: [X]
-
-**Status:** [ ] Pass / [ ] Fail
-
-**Notes:**
-```
-[Any observations]
-```
-
----
-
-### Test 5: Scrape Docs (Small Test)
-
-**Command:**
-```
-Scrape docs using configs/kubernetes.json with max 10 pages
-```
-
-**Expected Result:**
-- Creates `output/kubernetes_data/` directory
-- Creates `output/kubernetes/` skill directory
-- Generates `output/kubernetes/SKILL.md`
-- Creates reference files in `output/kubernetes/references/`
-- Completes in ~1-2 minutes (for 10 pages)
-- No errors during scraping
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**Files Created:**
-```bash
-# Check directories
-ls output/kubernetes_data/
-ls output/kubernetes/
-ls output/kubernetes/references/
-
-# Check SKILL.md
-wc -l output/kubernetes/SKILL.md
-
-# Count reference files
-ls output/kubernetes/references/ | wc -l
-```
-
-**Performance:**
-- Time taken: [X minutes]
-- Pages scraped: [X]
-- Reference files created: [X]
-
-**Status:** [ ] Pass / [ ] Fail
-
-**Notes:**
-```
-[Any observations]
-```
-
----
-
-### Test 6: Package Skill
-
-**Command:**
-```
-Package skill at output/kubernetes/
-```
-
-**Expected Result:**
-- Creates `output/kubernetes.zip`
-- File is valid ZIP archive
-- Contains SKILL.md and references/
-- Size is reasonable (< 10 MB for 10 pages)
-- Completes in < 5 seconds
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**File Verification:**
-```bash
-# Check file exists
-ls -lh output/kubernetes.zip
-
-# Check ZIP contents
-unzip -l output/kubernetes.zip
-
-# Verify ZIP is valid
-unzip -t output/kubernetes.zip
-```
-
-**Performance:**
-- Time taken: [X seconds]
-- ZIP file size: [X MB]
-
-**Status:** [ ] Pass / [ ] Fail
-
-**Notes:**
-```
-[Any observations]
-```
-
----
-
-## Additional Tests
-
-### Test 7: Error Handling - Invalid Config
-
-**Command:**
-```
-Validate configs/nonexistent.json
-```
-
-**Expected Result:**
-- Shows clear error message
-- Does not crash
-- Suggests checking file path
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**Status:** [ ] Pass / [ ] Fail
-
----
-
-### Test 8: Error Handling - Invalid URL
-
-**Command:**
-```
-Generate config for Test at not-a-valid-url
-```
-
-**Expected Result:**
-- Shows error about invalid URL
-- Does not create config file
-- Does not crash
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**Status:** [ ] Pass / [ ] Fail
-
----
-
-### Test 9: Concurrent Tool Calls
-
-**Commands (rapid succession):**
-```
-1. List all available configs
-2. Validate configs/react.json
-3. Validate configs/vue.json
-```
-
-**Expected Result:**
-- All commands execute successfully
-- No race conditions
-- Responses are correct for each command
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**Status:** [ ] Pass / [ ] Fail
-
----
-
-### Test 10: Large Scrape Operation
-
-**Command:**
-```
-Scrape docs using configs/react.json with max 100 pages
-```
-
-**Expected Result:**
-- Handles long-running operation (10-15 minutes)
-- Shows progress or remains responsive
-- Completes successfully
-- Creates comprehensive skill
-- No memory leaks
-
-**Actual Result:**
-```
-[To be filled]
-```
-
-**Performance:**
-- Time taken: [X minutes]
-- Pages scraped: [X]
-- Memory usage: [X MB]
-- Peak memory: [X MB]
-
-**Status:** [ ] Pass / [ ] Fail
-
----
-
-## Performance Metrics
-
-| Operation | Expected Time | Actual Time | Status |
-|-----------|--------------|-------------|--------|
-| List configs | < 1s | [X]s | [ ] |
-| Validate config | < 2s | [X]s | [ ] |
-| Generate config | < 3s | [X]s | [ ] |
-| Estimate pages (100) | 30-60s | [X]s | [ ] |
-| Scrape 10 pages | 1-2 min | [X]min | [ ] |
-| Scrape 100 pages | 10-15 min | [X]min | [ ] |
-| Package skill | < 5s | [X]s | [ ] |
-
----
-
-## Issues Found
-
-### Issue 1: [Title]
-
-**Severity:** [ ] Critical / [ ] High / [ ] Medium / [ ] Low
-
-**Description:**
-```
-[Detailed description of the issue]
-```
-
-**Steps to Reproduce:**
-1. [Step 1]
-2. [Step 2]
-3. [Step 3]
-
-**Expected Behavior:**
-```
-[What should happen]
-```
-
-**Actual Behavior:**
-```
-[What actually happened]
-```
-
-**Error Messages:**
-```
-[Any error messages or logs]
-```
-
-**Workaround:**
-```
-[Temporary solution, if any]
-```
-
-**Fix Required:** [ ] Yes / [ ] No
-
----
-
-### Issue 2: [Title]
-
-[Same format as Issue 1]
-
----
-
-## Configuration Used
-
-```json
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "python3",
- "args": [
- "/path/to/Skill_Seekers/mcp/server.py"
- ],
- "cwd": "/path/to/Skill_Seekers"
- }
- }
-}
-```
-
----
-
-## Summary
-
-**Total Tests:** 10
-**Tests Passed:** [X]
-**Tests Failed:** [X]
-**Tests Skipped:** [X]
-
-**Overall Status:** [ ] Pass / [ ] Fail / [ ] Partial
-
-**Recommendation:**
-```
-[Ready for production / Needs fixes / Requires more testing]
-```
-
----
-
-## Observations
-
-### What Worked Well
-- [Observation 1]
-- [Observation 2]
-- [Observation 3]
-
-### What Needs Improvement
-- [Observation 1]
-- [Observation 2]
-- [Observation 3]
-
-### Suggestions
-- [Suggestion 1]
-- [Suggestion 2]
-- [Suggestion 3]
-
----
-
-## Next Steps
-
-- [ ] Address critical issues
-- [ ] Re-test failed cases
-- [ ] Document workarounds
-- [ ] Update MCP server if needed
-- [ ] Update documentation based on findings
-- [ ] Create GitHub issues for bugs found
-
----
-
-## Appendix: Test Commands Reference
-
-```bash
-# Quick test sequence
-echo "Test 1: List configs"
-# User says: "List all available configs"
-
-echo "Test 2: Validate"
-# User says: "Validate configs/react.json"
-
-echo "Test 3: Generate"
-# User says: "Generate config for Tailwind CSS at https://tailwindcss.com/docs"
-
-echo "Test 4: Estimate"
-# User says: "Estimate pages for configs/tailwind.json"
-
-echo "Test 5: Scrape"
-# User says: "Scrape docs using configs/tailwind.json with max 10 pages"
-
-echo "Test 6: Package"
-# User says: "Package skill at output/tailwind/"
-
-# Verify results
-ls configs/tailwind.json
-ls output/tailwind/SKILL.md
-ls output/tailwind.zip
-```
-
----
-
-## Test Environment Setup Script
-
-```bash
-#!/bin/bash
-# Test environment setup
-
-echo "Setting up MCP integration test environment..."
-
-# 1. Check prerequisites
-echo "Checking Python version..."
-python3 --version
-
-echo "Checking Claude Code..."
-# (Manual check required)
-
-# 2. Install dependencies
-echo "Installing dependencies..."
-pip3 install -r mcp/requirements.txt
-pip3 install requests beautifulsoup4
-
-# 3. Verify installation
-echo "Verifying MCP server..."
-timeout 2 python3 mcp/server.py || echo "Server can start"
-
-# 4. Create test output directory
-echo "Creating test directories..."
-mkdir -p test_output
-
-echo "Setup complete! Ready for testing."
-echo "Next: Configure Claude Code MCP settings and restart"
-```
-
----
-
-## Cleanup Script
-
-```bash
-#!/bin/bash
-# Cleanup after tests
-
-echo "Cleaning up test artifacts..."
-
-# Remove test configs
-rm -f configs/tailwind.json
-rm -f configs/test*.json
-
-# Remove test output
-rm -rf output/tailwind*
-rm -rf output/kubernetes*
-rm -rf test_output
-
-echo "Cleanup complete!"
-```
-
----
-
-**Testing Status:** [ ] Not Started / [ ] In Progress / [ ] Completed
-
-**Sign-off:**
-- Tester: [Name]
-- Date: [YYYY-MM-DD]
-- Approved: [ ] Yes / [ ] No
diff --git a/assets/repo/Skill_Seekers-development/tests/test_async_scraping.py b/assets/repo/Skill_Seekers-development/tests/test_async_scraping.py
deleted file mode 100644
index b5ee9f2..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_async_scraping.py
+++ /dev/null
@@ -1,328 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for async scraping functionality
-Tests the async/await implementation for parallel web scraping
-"""
-
-import sys
-import os
-import unittest
-import asyncio
-import tempfile
-from pathlib import Path
-from unittest.mock import Mock, patch, AsyncMock, MagicMock
-from collections import deque
-
-from skill_seekers.cli.doc_scraper import DocToSkillConverter
-
-
-class TestAsyncConfiguration(unittest.TestCase):
- """Test async mode configuration and initialization"""
-
- def setUp(self):
- """Save original working directory"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Restore original working directory"""
- os.chdir(self.original_cwd)
-
- def test_async_mode_default_false(self):
- """Test async mode is disabled by default"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': 10
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertFalse(converter.async_mode)
- finally:
- os.chdir(self.original_cwd)
-
- def test_async_mode_enabled_from_config(self):
- """Test async mode can be enabled via config"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': 10,
- 'async_mode': True
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertTrue(converter.async_mode)
- finally:
- os.chdir(self.original_cwd)
-
- def test_async_mode_with_workers(self):
- """Test async mode works with multiple workers"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'workers': 4,
- 'async_mode': True
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertTrue(converter.async_mode)
- self.assertEqual(converter.workers, 4)
- finally:
- os.chdir(self.original_cwd)
-
-
-class TestAsyncScrapeMethods(unittest.TestCase):
- """Test async scraping methods exist and have correct signatures"""
-
- def setUp(self):
- """Set up test fixtures"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Clean up"""
- os.chdir(self.original_cwd)
-
- def test_scrape_page_async_exists(self):
- """Test scrape_page_async method exists"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'}
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertTrue(hasattr(converter, 'scrape_page_async'))
- self.assertTrue(asyncio.iscoroutinefunction(converter.scrape_page_async))
- finally:
- os.chdir(self.original_cwd)
-
- def test_scrape_all_async_exists(self):
- """Test scrape_all_async method exists"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'}
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertTrue(hasattr(converter, 'scrape_all_async'))
- self.assertTrue(asyncio.iscoroutinefunction(converter.scrape_all_async))
- finally:
- os.chdir(self.original_cwd)
-
-
-class TestAsyncRouting(unittest.TestCase):
- """Test that scrape_all() correctly routes to async version"""
-
- def setUp(self):
- """Set up test fixtures"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Clean up"""
- os.chdir(self.original_cwd)
-
- def test_scrape_all_routes_to_async_when_enabled(self):
- """Test scrape_all calls async version when async_mode=True"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'async_mode': True,
- 'max_pages': 1
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
-
- # Mock scrape_all_async to verify it gets called
- with patch.object(converter, 'scrape_all_async', new_callable=AsyncMock) as mock_async:
- converter.scrape_all()
- # Verify async version was called
- mock_async.assert_called_once()
- finally:
- os.chdir(self.original_cwd)
-
- def test_scrape_all_uses_sync_when_async_disabled(self):
- """Test scrape_all uses sync version when async_mode=False"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'async_mode': False,
- 'max_pages': 1
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
-
- # Mock scrape_all_async to verify it does NOT get called
- with patch.object(converter, 'scrape_all_async', new_callable=AsyncMock) as mock_async:
- with patch.object(converter, '_try_llms_txt', return_value=False):
- converter.scrape_all()
- # Verify async version was NOT called
- mock_async.assert_not_called()
- finally:
- os.chdir(self.original_cwd)
-
-
-class TestAsyncDryRun(unittest.TestCase):
- """Test async scraping in dry-run mode"""
-
- def setUp(self):
- """Set up test fixtures"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Clean up"""
- os.chdir(self.original_cwd)
-
- def test_async_dry_run_completes(self):
- """Test async dry run completes without errors"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'async_mode': True,
- 'max_pages': 5
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
-
- # Mock _try_llms_txt to skip llms.txt detection
- with patch.object(converter, '_try_llms_txt', return_value=False):
- # Should complete without errors
- converter.scrape_all()
- # Verify dry run mode was used
- self.assertTrue(converter.dry_run)
- finally:
- os.chdir(self.original_cwd)
-
-
-class TestAsyncErrorHandling(unittest.TestCase):
- """Test error handling in async scraping"""
-
- def setUp(self):
- """Set up test fixtures"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Clean up"""
- os.chdir(self.original_cwd)
-
- def test_async_handles_http_errors(self):
- """Test async scraping handles HTTP errors gracefully"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'async_mode': True,
- 'workers': 2,
- 'max_pages': 1
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=False)
-
- # Mock httpx to simulate errors
- import httpx
-
- async def run_test():
- semaphore = asyncio.Semaphore(2)
-
- async with httpx.AsyncClient() as client:
- # Mock client.get to raise exception
- with patch.object(client, 'get', side_effect=httpx.HTTPError("Test error")):
- # Should not raise exception, just log error
- await converter.scrape_page_async('https://example.com/test', semaphore, client)
-
- # Run async test
- asyncio.run(run_test())
- # If we got here without exception, test passed
- finally:
- os.chdir(self.original_cwd)
-
-
-class TestAsyncPerformance(unittest.TestCase):
- """Test async performance characteristics"""
-
- def test_async_uses_semaphore_for_concurrency_control(self):
- """Test async mode uses semaphore instead of threading lock"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'async_mode': True,
- 'workers': 4
- }
-
- original_cwd = os.getcwd()
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
-
- # Async mode should NOT create threading lock
- # (async uses asyncio.Semaphore instead)
- self.assertTrue(converter.async_mode)
- finally:
- os.chdir(original_cwd)
-
-
-class TestAsyncLlmsTxtIntegration(unittest.TestCase):
- """Test async mode with llms.txt detection"""
-
- def test_async_respects_llms_txt(self):
- """Test async mode respects llms.txt and skips HTML scraping"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'async_mode': True
- }
-
- original_cwd = os.getcwd()
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=False)
-
- # Mock _try_llms_txt to return True (llms.txt found)
- with patch.object(converter, '_try_llms_txt', return_value=True):
- with patch.object(converter, 'save_summary'):
- converter.scrape_all()
- # If llms.txt succeeded, async scraping should be skipped
- # Verify by checking that pages were not scraped
- self.assertEqual(len(converter.visited_urls), 0)
- finally:
- os.chdir(original_cwd)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_cli_paths.py b/assets/repo/Skill_Seekers-development/tests/test_cli_paths.py
deleted file mode 100644
index 5a96e82..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_cli_paths.py
+++ /dev/null
@@ -1,189 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test suite for modern CLI command patterns
-Tests that all CLI scripts use correct unified CLI commands in usage messages and print statements
-"""
-
-import sys
-import os
-import unittest
-import subprocess
-from pathlib import Path
-
-# Add parent directory to path
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-
-class TestModernCLICommands(unittest.TestCase):
- """Test that all CLI scripts use modern unified CLI commands"""
-
- def test_doc_scraper_uses_modern_commands(self):
- """Test doc_scraper.py uses skill-seekers commands"""
- script_path = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli' / 'doc_scraper.py'
-
- with open(script_path, 'r') as f:
- content = f.read()
-
- # Should use modern commands
- self.assertIn('skill-seekers scrape', content)
-
- # Should NOT use old python3 cli/ pattern
- self.assertNotIn('python3 cli/doc_scraper.py', content)
-
- def test_enhance_skill_local_uses_modern_commands(self):
- """Test enhance_skill_local.py uses skill-seekers commands"""
- script_path = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli' / 'enhance_skill_local.py'
-
- with open(script_path, 'r') as f:
- content = f.read()
-
- # Should use modern commands
- self.assertIn('skill-seekers', content)
-
- # Should NOT use old python3 cli/ pattern
- self.assertNotIn('python3 cli/enhance_skill_local.py', content)
-
- def test_estimate_pages_uses_modern_commands(self):
- """Test estimate_pages.py uses skill-seekers commands"""
- script_path = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli' / 'estimate_pages.py'
-
- with open(script_path, 'r') as f:
- content = f.read()
-
- # Should use modern commands
- self.assertIn('skill-seekers estimate', content)
-
- # Should NOT use old python3 cli/ pattern
- self.assertNotIn('python3 cli/estimate_pages.py', content)
-
- def test_package_skill_uses_modern_commands(self):
- """Test package_skill.py uses skill-seekers commands"""
- script_path = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli' / 'package_skill.py'
-
- with open(script_path, 'r') as f:
- content = f.read()
-
- # Should use modern commands
- self.assertIn('skill-seekers package', content)
-
- # Should NOT use old python3 cli/ pattern
- self.assertNotIn('python3 cli/package_skill.py', content)
-
- def test_github_scraper_uses_modern_commands(self):
- """Test github_scraper.py uses skill-seekers commands"""
- script_path = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli' / 'github_scraper.py'
-
- with open(script_path, 'r') as f:
- content = f.read()
-
- # Should use modern commands
- self.assertIn('skill-seekers', content)
-
- # Should NOT use old python3 cli/ pattern
- self.assertNotIn('python3 cli/github_scraper.py', content)
-
-
-class TestUnifiedCLIEntryPoints(unittest.TestCase):
- """Test that unified CLI entry points work correctly"""
-
- def test_main_cli_help_output(self):
- """Test skill-seekers --help works"""
- try:
- result = subprocess.run(
- ['skill-seekers', '--help'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # Should return successfully
- self.assertIn(result.returncode, [0, 2],
- f"skill-seekers --help failed with code {result.returncode}")
-
- # Should show subcommands
- output = result.stdout + result.stderr
- self.assertIn('scrape', output)
- self.assertIn('github', output)
- self.assertIn('package', output)
-
- except FileNotFoundError:
- # If skill-seekers is not installed, skip this test
- self.skipTest("skill-seekers command not found - install package first")
-
- def test_main_cli_version_output(self):
- """Test skill-seekers --version works"""
- try:
- result = subprocess.run(
- ['skill-seekers', '--version'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # Should return successfully
- self.assertEqual(result.returncode, 0,
- f"skill-seekers --version failed: {result.stderr}")
-
- # Should show version
- output = result.stdout + result.stderr
- self.assertIn('2.1.1', output)
-
- except FileNotFoundError:
- # If skill-seekers is not installed, skip this test
- self.skipTest("skill-seekers command not found - install package first")
-
-
-class TestNoHardcodedPaths(unittest.TestCase):
- """Test that no scripts have hardcoded absolute paths"""
-
- def test_no_hardcoded_paths_in_cli_scripts(self):
- """Test that CLI scripts don't have hardcoded paths"""
- cli_dir = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli'
-
- hardcoded_paths = [
- '/mnt/skills/examples/skill-creator/scripts/',
- '/home/',
- '/Users/',
- ]
-
- for script_path in cli_dir.glob('*.py'):
- with open(script_path, 'r') as f:
- content = f.read()
-
- for hardcoded_path in hardcoded_paths:
- self.assertNotIn(hardcoded_path, content,
- f"{script_path.name} contains hardcoded path: {hardcoded_path}")
-
-
-class TestPackageStructure(unittest.TestCase):
- """Test that package structure is correct"""
-
- def test_src_layout_exists(self):
- """Test that src/ layout directory exists"""
- src_dir = Path(__file__).parent.parent / 'src' / 'skill_seekers'
- self.assertTrue(src_dir.exists(), "src/skill_seekers/ directory should exist")
-
- def test_cli_package_exists(self):
- """Test that CLI package exists in src/"""
- cli_dir = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli'
- self.assertTrue(cli_dir.exists(), "src/skill_seekers/cli/ directory should exist")
-
- init_file = cli_dir / '__init__.py'
- self.assertTrue(init_file.exists(), "src/skill_seekers/cli/__init__.py should exist")
-
- def test_mcp_package_exists(self):
- """Test that MCP package exists in src/"""
- mcp_dir = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'mcp'
- self.assertTrue(mcp_dir.exists(), "src/skill_seekers/mcp/ directory should exist")
-
- init_file = mcp_dir / '__init__.py'
- self.assertTrue(init_file.exists(), "src/skill_seekers/mcp/__init__.py should exist")
-
- def test_main_cli_file_exists(self):
- """Test that main.py unified CLI exists"""
- main_file = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli' / 'main.py'
- self.assertTrue(main_file.exists(), "src/skill_seekers/cli/main.py should exist")
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_config_validation.py b/assets/repo/Skill_Seekers-development/tests/test_config_validation.py
deleted file mode 100644
index eaae6ec..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_config_validation.py
+++ /dev/null
@@ -1,336 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test suite for configuration validation
-Tests the validate_config() function with various valid and invalid configs
-"""
-
-import sys
-import os
-import unittest
-
-# Add parent directory to path
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.doc_scraper import validate_config
-
-
-class TestConfigValidation(unittest.TestCase):
- """Test configuration validation"""
-
- def test_valid_minimal_config(self):
- """Test valid minimal configuration"""
- config = {
- 'name': 'test-skill',
- 'base_url': 'https://example.com/'
- }
- errors, _ = validate_config(config)
- # Should have warnings about missing selectors, but no critical errors
- self.assertIsInstance(errors, list)
-
- def test_valid_complete_config(self):
- """Test valid complete configuration"""
- config = {
- 'name': 'godot',
- 'base_url': 'https://docs.godotengine.org/en/stable/',
- 'description': 'Godot Engine documentation',
- 'selectors': {
- 'main_content': 'div[role="main"]',
- 'title': 'title',
- 'code_blocks': 'pre code'
- },
- 'url_patterns': {
- 'include': ['/guide/', '/api/'],
- 'exclude': ['/blog/']
- },
- 'categories': {
- 'getting_started': ['intro', 'tutorial'],
- 'api': ['api', 'reference']
- },
- 'rate_limit': 0.5,
- 'max_pages': 500
- }
- errors, _ = validate_config(config)
- self.assertEqual(len(errors), 0, f"Valid config should have no errors, got: {errors}")
-
- def test_missing_name(self):
- """Test missing required field 'name'"""
- config = {
- 'base_url': 'https://example.com/'
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('name' in error.lower() for error in errors))
-
- def test_missing_base_url(self):
- """Test missing required field 'base_url'"""
- config = {
- 'name': 'test'
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('base_url' in error.lower() for error in errors))
-
- def test_invalid_name_special_chars(self):
- """Test invalid name with special characters"""
- config = {
- 'name': 'test@skill!',
- 'base_url': 'https://example.com/'
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('invalid name' in error.lower() for error in errors))
-
- def test_valid_name_formats(self):
- """Test various valid name formats"""
- valid_names = ['test', 'test-skill', 'test_skill', 'TestSkill123', 'my-awesome-skill_v2']
- for name in valid_names:
- config = {
- 'name': name,
- 'base_url': 'https://example.com/'
- }
- errors, _ = validate_config(config)
- name_errors = [e for e in errors if 'invalid name' in e.lower()]
- self.assertEqual(len(name_errors), 0, f"Name '{name}' should be valid")
-
- def test_invalid_base_url_no_protocol(self):
- """Test invalid base_url without protocol"""
- config = {
- 'name': 'test',
- 'base_url': 'example.com'
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('base_url' in error.lower() for error in errors))
-
- def test_valid_url_protocols(self):
- """Test valid URL protocols"""
- for protocol in ['http://', 'https://']:
- config = {
- 'name': 'test',
- 'base_url': f'{protocol}example.com/'
- }
- errors, _ = validate_config(config)
- url_errors = [e for e in errors if 'base_url' in e.lower() and 'invalid' in e.lower()]
- self.assertEqual(len(url_errors), 0, f"Protocol '{protocol}' should be valid")
-
- def test_invalid_selectors_not_dict(self):
- """Test invalid selectors (not a dictionary)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': 'invalid'
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('selectors' in error.lower() and 'dictionary' in error.lower() for error in errors))
-
- def test_missing_recommended_selectors(self):
- """Test warning for missing recommended selectors"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {
- 'main_content': 'article'
- # Missing 'title' and 'code_blocks'
- }
- }
- _, warnings = validate_config(config)
- self.assertTrue(any('title' in warning.lower() for warning in warnings))
- self.assertTrue(any('code_blocks' in warning.lower() for warning in warnings))
-
- def test_invalid_url_patterns_not_dict(self):
- """Test invalid url_patterns (not a dictionary)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'url_patterns': []
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('url_patterns' in error.lower() and 'dictionary' in error.lower() for error in errors))
-
- def test_invalid_url_patterns_include_not_list(self):
- """Test invalid url_patterns.include (not a list)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'url_patterns': {
- 'include': 'not-a-list'
- }
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('include' in error.lower() and 'list' in error.lower() for error in errors))
-
- def test_invalid_categories_not_dict(self):
- """Test invalid categories (not a dictionary)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'categories': []
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('categories' in error.lower() and 'dictionary' in error.lower() for error in errors))
-
- def test_invalid_category_keywords_not_list(self):
- """Test invalid category keywords (not a list)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'categories': {
- 'getting_started': 'not-a-list'
- }
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('getting_started' in error.lower() and 'list' in error.lower() for error in errors))
-
- def test_invalid_rate_limit_negative(self):
- """Test invalid rate_limit (negative)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'rate_limit': -1
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('rate_limit' in error.lower() for error in errors))
-
- def test_invalid_rate_limit_too_high(self):
- """Test invalid rate_limit (too high)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'rate_limit': 20
- }
- _, warnings = validate_config(config)
- self.assertTrue(any('rate_limit' in warning.lower() for warning in warnings))
-
- def test_invalid_rate_limit_not_number(self):
- """Test invalid rate_limit (not a number)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'rate_limit': 'fast'
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('rate_limit' in error.lower() for error in errors))
-
- def test_valid_rate_limit_range(self):
- """Test valid rate_limit range"""
- for rate in [0, 0.1, 0.5, 1, 5, 10]:
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'rate_limit': rate
- }
- errors, _ = validate_config(config)
- rate_errors = [e for e in errors if 'rate_limit' in e.lower()]
- self.assertEqual(len(rate_errors), 0, f"Rate limit {rate} should be valid")
-
- def test_invalid_max_pages_zero(self):
- """Test invalid max_pages (zero)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'max_pages': 0
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('max_pages' in error.lower() for error in errors))
-
- def test_invalid_max_pages_too_high(self):
- """Test invalid max_pages (too high)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'max_pages': 20000
- }
- _, warnings = validate_config(config)
- self.assertTrue(any('max_pages' in warning.lower() for warning in warnings))
-
- def test_invalid_max_pages_not_int(self):
- """Test invalid max_pages (not an integer)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'max_pages': 'many'
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('max_pages' in error.lower() for error in errors))
-
- def test_valid_max_pages_range(self):
- """Test valid max_pages range"""
- for max_p in [1, 10, 100, 500, 5000, 10000]:
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'max_pages': max_p
- }
- errors, _ = validate_config(config)
- max_errors = [e for e in errors if 'max_pages' in e.lower()]
- self.assertEqual(len(max_errors), 0, f"Max pages {max_p} should be valid")
-
- def test_invalid_start_urls_not_list(self):
- """Test invalid start_urls (not a list)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'start_urls': 'https://example.com/page1'
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('start_urls' in error.lower() and 'list' in error.lower() for error in errors))
-
- def test_invalid_start_urls_bad_protocol(self):
- """Test invalid start_urls (bad protocol)"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'start_urls': ['ftp://example.com/page1']
- }
- errors, _ = validate_config(config)
- self.assertTrue(any('start_url' in error.lower() for error in errors))
-
- def test_valid_start_urls(self):
- """Test valid start_urls"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'start_urls': [
- 'https://example.com/page1',
- 'http://example.com/page2',
- 'https://example.com/api/docs'
- ]
- }
- errors, _ = validate_config(config)
- url_errors = [e for e in errors if 'start_url' in e.lower()]
- self.assertEqual(len(url_errors), 0, "Valid start_urls should pass validation")
-
- def test_config_with_llms_txt_url(self):
- """Test config validation with explicit llms_txt_url"""
- config = {
- 'name': 'test',
- 'llms_txt_url': 'https://example.com/llms-full.txt',
- 'base_url': 'https://example.com/docs'
- }
-
- # Should be valid
- self.assertEqual(config.get('llms_txt_url'), 'https://example.com/llms-full.txt')
-
- def test_config_with_skip_llms_txt(self):
- """Test config validation accepts skip_llms_txt"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/docs',
- 'skip_llms_txt': True
- }
-
- errors, warnings = validate_config(config)
- self.assertEqual(errors, [])
- self.assertTrue(config.get('skip_llms_txt'))
-
- def test_config_with_skip_llms_txt_false(self):
- """Test config validation accepts skip_llms_txt as False"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/docs',
- 'skip_llms_txt': False
- }
-
- errors, warnings = validate_config(config)
- self.assertEqual(errors, [])
- self.assertFalse(config.get('skip_llms_txt'))
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_constants.py b/assets/repo/Skill_Seekers-development/tests/test_constants.py
deleted file mode 100644
index 0eef01f..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_constants.py
+++ /dev/null
@@ -1,163 +0,0 @@
-#!/usr/bin/env python3
-"""Test suite for cli/constants.py module."""
-
-import unittest
-import sys
-from pathlib import Path
-
-# Add parent directory to path
-sys.path.insert(0, str(Path(__file__).parent.parent))
-
-from skill_seekers.cli.constants import (
- DEFAULT_RATE_LIMIT,
- DEFAULT_MAX_PAGES,
- DEFAULT_CHECKPOINT_INTERVAL,
- CONTENT_PREVIEW_LENGTH,
- MAX_PAGES_WARNING_THRESHOLD,
- MIN_CATEGORIZATION_SCORE,
- URL_MATCH_POINTS,
- TITLE_MATCH_POINTS,
- CONTENT_MATCH_POINTS,
- API_CONTENT_LIMIT,
- API_PREVIEW_LIMIT,
- LOCAL_CONTENT_LIMIT,
- LOCAL_PREVIEW_LIMIT,
- DEFAULT_MAX_DISCOVERY,
- DISCOVERY_THRESHOLD,
- MAX_REFERENCE_FILES,
- MAX_CODE_BLOCKS_PER_PAGE,
-)
-
-
-class TestConstants(unittest.TestCase):
- """Test that all constants are defined and have sensible values."""
-
- def test_scraping_constants_exist(self):
- """Test that scraping constants are defined."""
- self.assertIsNotNone(DEFAULT_RATE_LIMIT)
- self.assertIsNotNone(DEFAULT_MAX_PAGES)
- self.assertIsNotNone(DEFAULT_CHECKPOINT_INTERVAL)
-
- def test_scraping_constants_types(self):
- """Test that scraping constants have correct types."""
- self.assertIsInstance(DEFAULT_RATE_LIMIT, (int, float))
- self.assertIsInstance(DEFAULT_MAX_PAGES, int)
- self.assertIsInstance(DEFAULT_CHECKPOINT_INTERVAL, int)
-
- def test_scraping_constants_ranges(self):
- """Test that scraping constants have sensible values."""
- self.assertGreater(DEFAULT_RATE_LIMIT, 0)
- self.assertGreater(DEFAULT_MAX_PAGES, 0)
- self.assertGreater(DEFAULT_CHECKPOINT_INTERVAL, 0)
- self.assertEqual(DEFAULT_RATE_LIMIT, 0.5)
- self.assertEqual(DEFAULT_MAX_PAGES, 500)
- self.assertEqual(DEFAULT_CHECKPOINT_INTERVAL, 1000)
-
- def test_content_analysis_constants(self):
- """Test content analysis constants."""
- self.assertEqual(CONTENT_PREVIEW_LENGTH, 500)
- self.assertEqual(MAX_PAGES_WARNING_THRESHOLD, 10000)
- self.assertGreater(MAX_PAGES_WARNING_THRESHOLD, DEFAULT_MAX_PAGES)
-
- def test_categorization_constants(self):
- """Test categorization scoring constants."""
- self.assertEqual(MIN_CATEGORIZATION_SCORE, 2)
- self.assertEqual(URL_MATCH_POINTS, 3)
- self.assertEqual(TITLE_MATCH_POINTS, 2)
- self.assertEqual(CONTENT_MATCH_POINTS, 1)
- # Verify scoring hierarchy
- self.assertGreater(URL_MATCH_POINTS, TITLE_MATCH_POINTS)
- self.assertGreater(TITLE_MATCH_POINTS, CONTENT_MATCH_POINTS)
-
- def test_enhancement_constants_exist(self):
- """Test that enhancement constants are defined."""
- self.assertIsNotNone(API_CONTENT_LIMIT)
- self.assertIsNotNone(API_PREVIEW_LIMIT)
- self.assertIsNotNone(LOCAL_CONTENT_LIMIT)
- self.assertIsNotNone(LOCAL_PREVIEW_LIMIT)
-
- def test_enhancement_constants_values(self):
- """Test enhancement constants have expected values."""
- self.assertEqual(API_CONTENT_LIMIT, 100000)
- self.assertEqual(API_PREVIEW_LIMIT, 40000)
- self.assertEqual(LOCAL_CONTENT_LIMIT, 50000)
- self.assertEqual(LOCAL_PREVIEW_LIMIT, 20000)
-
- def test_enhancement_limits_hierarchy(self):
- """Test that API limits are higher than local limits."""
- self.assertGreater(API_CONTENT_LIMIT, LOCAL_CONTENT_LIMIT)
- self.assertGreater(API_PREVIEW_LIMIT, LOCAL_PREVIEW_LIMIT)
- self.assertGreater(API_CONTENT_LIMIT, API_PREVIEW_LIMIT)
- self.assertGreater(LOCAL_CONTENT_LIMIT, LOCAL_PREVIEW_LIMIT)
-
- def test_estimation_constants(self):
- """Test page estimation constants."""
- self.assertEqual(DEFAULT_MAX_DISCOVERY, 1000)
- self.assertEqual(DISCOVERY_THRESHOLD, 10000)
- self.assertGreater(DISCOVERY_THRESHOLD, DEFAULT_MAX_DISCOVERY)
-
- def test_file_limit_constants(self):
- """Test file limit constants."""
- self.assertEqual(MAX_REFERENCE_FILES, 100)
- self.assertEqual(MAX_CODE_BLOCKS_PER_PAGE, 5)
- self.assertGreater(MAX_REFERENCE_FILES, 0)
- self.assertGreater(MAX_CODE_BLOCKS_PER_PAGE, 0)
-
-
-class TestConstantsUsage(unittest.TestCase):
- """Test that constants are properly used in other modules."""
-
- def test_doc_scraper_imports_constants(self):
- """Test that doc_scraper imports and uses constants."""
- from skill_seekers.cli import doc_scraper
- # Check that doc_scraper can access the constants
- self.assertTrue(hasattr(doc_scraper, 'DEFAULT_RATE_LIMIT'))
- self.assertTrue(hasattr(doc_scraper, 'DEFAULT_MAX_PAGES'))
-
- def test_estimate_pages_imports_constants(self):
- """Test that estimate_pages imports and uses constants."""
- from skill_seekers.cli import estimate_pages
- # Verify function signature uses constants
- import inspect
- sig = inspect.signature(estimate_pages.estimate_pages)
- self.assertIn('max_discovery', sig.parameters)
-
- def test_enhance_skill_imports_constants(self):
- """Test that enhance_skill imports constants."""
- try:
- from skill_seekers.cli import enhance_skill
- # Check module loads without errors
- self.assertIsNotNone(enhance_skill)
- except (ImportError, SystemExit) as e:
- # anthropic package may not be installed or module exits on import
- # This is acceptable - we're just checking the constants import works
- pass
-
- def test_enhance_skill_local_imports_constants(self):
- """Test that enhance_skill_local imports constants."""
- from skill_seekers.cli import enhance_skill_local
- self.assertIsNotNone(enhance_skill_local)
-
-
-class TestConstantsExports(unittest.TestCase):
- """Test that constants module exports are correct."""
-
- def test_all_exports_exist(self):
- """Test that all items in __all__ exist."""
- from skill_seekers.cli import constants
- self.assertTrue(hasattr(constants, '__all__'))
- for name in constants.__all__:
- self.assertTrue(
- hasattr(constants, name),
- f"Constant '{name}' in __all__ but not defined"
- )
-
- def test_all_exports_count(self):
- """Test that __all__ has expected number of exports."""
- from skill_seekers.cli import constants
- # We defined 18 constants (added DEFAULT_ASYNC_MODE)
- self.assertEqual(len(constants.__all__), 18)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_estimate_pages.py b/assets/repo/Skill_Seekers-development/tests/test_estimate_pages.py
deleted file mode 100644
index e9a8fa4..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_estimate_pages.py
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for cli/estimate_pages.py functionality
-"""
-
-import unittest
-import tempfile
-import json
-from pathlib import Path
-import sys
-
-from skill_seekers.cli.estimate_pages import estimate_pages
-
-
-class TestEstimatePages(unittest.TestCase):
- """Test estimate_pages function"""
-
- def test_estimate_pages_with_minimal_config(self):
- """Test estimation with minimal configuration"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'rate_limit': 0.1
- }
-
- # This will make real HTTP request to example.com
- # We use low max_discovery to keep test fast
- result = estimate_pages(config, max_discovery=2, timeout=5)
-
- # Check result structure
- self.assertIsInstance(result, dict)
- self.assertIn('discovered', result)
- self.assertIn('estimated_total', result)
- # Actual key is elapsed_seconds, not time_elapsed
- self.assertIn('elapsed_seconds', result)
-
- def test_estimate_pages_returns_discovered_count(self):
- """Test that result contains discovered page count"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'rate_limit': 0.1
- }
-
- result = estimate_pages(config, max_discovery=1, timeout=5)
-
- self.assertGreaterEqual(result['discovered'], 0)
- self.assertIsInstance(result['discovered'], int)
-
- def test_estimate_pages_respects_max_discovery(self):
- """Test that estimation respects max_discovery limit"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'rate_limit': 0.1
- }
-
- result = estimate_pages(config, max_discovery=3, timeout=5)
-
- # Should not discover more than max_discovery
- self.assertLessEqual(result['discovered'], 3)
-
- def test_estimate_pages_with_start_urls(self):
- """Test estimation with custom start_urls"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'start_urls': ['https://example.com/'],
- 'rate_limit': 0.1
- }
-
- result = estimate_pages(config, max_discovery=2, timeout=5)
-
- self.assertIsInstance(result, dict)
- self.assertIn('discovered', result)
-
-
-class TestEstimatePagesCLI(unittest.TestCase):
- """Test estimate_pages command-line interface (via entry point)"""
-
- def test_cli_help_output(self):
- """Test that skill-seekers estimate --help works"""
- import subprocess
-
- try:
- result = subprocess.run(
- ['skill-seekers', 'estimate', '--help'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # Should return successfully (0 or 2 for argparse)
- self.assertIn(result.returncode, [0, 2])
- output = result.stdout + result.stderr
- self.assertTrue('usage:' in output.lower() or 'estimate' in output.lower())
- except FileNotFoundError:
- self.skipTest("skill-seekers command not installed")
-
- def test_cli_executes_with_help_flag(self):
- """Test that skill-seekers-estimate entry point works"""
- import subprocess
-
- try:
- result = subprocess.run(
- ['skill-seekers-estimate', '--help'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # Should return successfully
- self.assertIn(result.returncode, [0, 2])
- except FileNotFoundError:
- self.skipTest("skill-seekers-estimate command not installed")
-
- def test_cli_requires_config_argument(self):
- """Test that CLI requires config file argument"""
- import subprocess
-
- try:
- # Run without config argument
- result = subprocess.run(
- ['skill-seekers', 'estimate'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # Should fail (non-zero exit code) or show usage
- self.assertTrue(
- result.returncode != 0 or 'usage' in result.stderr.lower() or 'usage' in result.stdout.lower()
- )
- except FileNotFoundError:
- self.skipTest("skill-seekers command not installed")
-
-
-class TestEstimatePagesWithRealConfig(unittest.TestCase):
- """Test estimation with real config files (if available)"""
-
- def test_estimate_with_real_config_file(self):
- """Test estimation using a real config file (if exists)"""
- config_path = Path('configs/react.json')
-
- if not config_path.exists():
- self.skipTest("configs/react.json not found")
-
- with open(config_path, 'r') as f:
- config = json.load(f)
-
- # Use very low max_discovery to keep test fast
- result = estimate_pages(config, max_discovery=3, timeout=5)
-
- self.assertIsInstance(result, dict)
- self.assertIn('discovered', result)
- self.assertGreater(result['discovered'], 0)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_excluded_dirs_config.py b/assets/repo/Skill_Seekers-development/tests/test_excluded_dirs_config.py
deleted file mode 100644
index 4fa1e58..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_excluded_dirs_config.py
+++ /dev/null
@@ -1,375 +0,0 @@
-"""Tests for configurable directory exclusions in GitHub scraper.
-
-Tests Issue #203: Make EXCLUDED_DIRS configurable
-"""
-
-import unittest
-from unittest.mock import patch, Mock
-from skill_seekers.cli.github_scraper import GitHubScraper, EXCLUDED_DIRS
-
-
-class TestExcludedDirsDefaults(unittest.TestCase):
- """Test default EXCLUDED_DIRS behavior (backward compatibility)."""
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_defaults_when_no_config(self, mock_github):
- """Test that default exclusions are used when no config provided."""
- config = {
- 'repo': 'owner/repo'
- }
-
- scraper = GitHubScraper(config)
-
- # Should use default EXCLUDED_DIRS
- self.assertEqual(scraper.excluded_dirs, EXCLUDED_DIRS)
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_defaults_exclude_common_dirs(self, mock_github):
- """Test that default exclusions work correctly."""
- config = {
- 'repo': 'owner/repo'
- }
-
- scraper = GitHubScraper(config)
-
- # Test common directories are excluded
- self.assertTrue(scraper.should_exclude_dir('venv'))
- self.assertTrue(scraper.should_exclude_dir('node_modules'))
- self.assertTrue(scraper.should_exclude_dir('__pycache__'))
- self.assertTrue(scraper.should_exclude_dir('.git'))
- self.assertTrue(scraper.should_exclude_dir('build'))
-
- # Test normal directories are not excluded
- self.assertFalse(scraper.should_exclude_dir('src'))
- self.assertFalse(scraper.should_exclude_dir('tests'))
- self.assertFalse(scraper.should_exclude_dir('docs'))
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_dot_directories_always_excluded(self, mock_github):
- """Test that directories starting with '.' are always excluded."""
- config = {
- 'repo': 'owner/repo'
- }
-
- scraper = GitHubScraper(config)
-
- # Dot directories should be excluded (even if not in EXCLUDED_DIRS)
- self.assertTrue(scraper.should_exclude_dir('.hidden'))
- self.assertTrue(scraper.should_exclude_dir('.cache'))
- self.assertTrue(scraper.should_exclude_dir('.vscode'))
-
-
-class TestExcludedDirsAdditional(unittest.TestCase):
- """Test exclude_dirs_additional (extend mode)."""
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_extend_with_additional_dirs(self, mock_github):
- """Test adding custom exclusions to defaults."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs_additional': ['proprietary', 'vendor', 'third_party']
- }
-
- scraper = GitHubScraper(config)
-
- # Should include both defaults and additional
- self.assertIn('venv', scraper.excluded_dirs) # Default
- self.assertIn('node_modules', scraper.excluded_dirs) # Default
- self.assertIn('proprietary', scraper.excluded_dirs) # Additional
- self.assertIn('vendor', scraper.excluded_dirs) # Additional
- self.assertIn('third_party', scraper.excluded_dirs) # Additional
-
- # Verify total count
- self.assertEqual(
- len(scraper.excluded_dirs),
- len(EXCLUDED_DIRS) + 3
- )
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_extend_excludes_additional_dirs(self, mock_github):
- """Test that additional directories are actually excluded."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs_additional': ['legacy', 'deprecated']
- }
-
- scraper = GitHubScraper(config)
-
- # Additional dirs should be excluded
- self.assertTrue(scraper.should_exclude_dir('legacy'))
- self.assertTrue(scraper.should_exclude_dir('deprecated'))
-
- # Default dirs still excluded
- self.assertTrue(scraper.should_exclude_dir('venv'))
- self.assertTrue(scraper.should_exclude_dir('node_modules'))
-
- # Normal dirs not excluded
- self.assertFalse(scraper.should_exclude_dir('src'))
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_extend_with_empty_list(self, mock_github):
- """Test that empty additional list works correctly."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs_additional': []
- }
-
- scraper = GitHubScraper(config)
-
- # Should just have defaults
- self.assertEqual(scraper.excluded_dirs, EXCLUDED_DIRS)
-
-
-class TestExcludedDirsReplace(unittest.TestCase):
- """Test exclude_dirs (replace mode)."""
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_replace_with_custom_list(self, mock_github):
- """Test replacing default exclusions entirely."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs': ['node_modules', 'custom_vendor']
- }
-
- scraper = GitHubScraper(config)
-
- # Should ONLY have specified dirs
- self.assertEqual(scraper.excluded_dirs, {'node_modules', 'custom_vendor'})
- self.assertEqual(len(scraper.excluded_dirs), 2)
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_replace_excludes_only_specified_dirs(self, mock_github):
- """Test that only specified directories are excluded in replace mode."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs': ['node_modules', '.git']
- }
-
- scraper = GitHubScraper(config)
-
- # Specified dirs should be excluded
- self.assertTrue(scraper.should_exclude_dir('node_modules'))
- # Note: .git would be excluded anyway due to dot prefix
- self.assertTrue(scraper.should_exclude_dir('.git'))
-
- # Default dirs NOT in our list should NOT be excluded
- self.assertFalse(scraper.should_exclude_dir('venv'))
- self.assertFalse(scraper.should_exclude_dir('__pycache__'))
- self.assertFalse(scraper.should_exclude_dir('build'))
-
- # Normal dirs still not excluded
- self.assertFalse(scraper.should_exclude_dir('src'))
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_replace_with_empty_list(self, mock_github):
- """Test that empty replace list allows all directories (except dot-prefixed)."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs': []
- }
-
- scraper = GitHubScraper(config)
-
- # No explicit exclusions
- self.assertEqual(scraper.excluded_dirs, set())
-
- # Nothing explicitly excluded
- self.assertFalse(scraper.should_exclude_dir('venv'))
- self.assertFalse(scraper.should_exclude_dir('node_modules'))
- self.assertFalse(scraper.should_exclude_dir('build'))
-
- # But dot dirs still excluded (different logic)
- self.assertTrue(scraper.should_exclude_dir('.git'))
- self.assertTrue(scraper.should_exclude_dir('.hidden'))
-
-
-class TestExcludedDirsPrecedence(unittest.TestCase):
- """Test precedence when both options provided."""
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_replace_takes_precedence_over_additional(self, mock_github):
- """Test that exclude_dirs takes precedence over exclude_dirs_additional."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs': ['only', 'these'], # Replace mode
- 'exclude_dirs_additional': ['ignored'] # Should be ignored
- }
-
- scraper = GitHubScraper(config)
-
- # Should use replace mode (exclude_dirs), ignore additional
- self.assertEqual(scraper.excluded_dirs, {'only', 'these'})
- self.assertNotIn('ignored', scraper.excluded_dirs)
- self.assertNotIn('venv', scraper.excluded_dirs) # Defaults also ignored
-
-
-class TestExcludedDirsEdgeCases(unittest.TestCase):
- """Test edge cases and error handling."""
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_duplicate_exclusions_in_additional(self, mock_github):
- """Test that duplicates in additional list are handled (set deduplication)."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs_additional': ['venv', 'custom', 'venv'] # venv is duplicate (default + listed)
- }
-
- scraper = GitHubScraper(config)
-
- # Should deduplicate automatically (using set)
- self.assertIn('venv', scraper.excluded_dirs)
- self.assertIn('custom', scraper.excluded_dirs)
- # Count should account for deduplication
- self.assertEqual(
- len(scraper.excluded_dirs),
- len(EXCLUDED_DIRS) + 1 # Only 'custom' is truly additional
- )
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_case_sensitive_exclusions(self, mock_github):
- """Test that exclusions are case-sensitive."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs': ['Venv', 'NODE_MODULES']
- }
-
- scraper = GitHubScraper(config)
-
- # Case-sensitive matching
- self.assertTrue(scraper.should_exclude_dir('Venv'))
- self.assertTrue(scraper.should_exclude_dir('NODE_MODULES'))
- self.assertFalse(scraper.should_exclude_dir('venv')) # Different case
- self.assertFalse(scraper.should_exclude_dir('node_modules')) # Different case
-
-
-class TestExcludedDirsWithLocalRepo(unittest.TestCase):
- """Test exclude_dirs integration with local_repo_path."""
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_exclude_dirs_with_local_repo_path(self, mock_github):
- """Test that exclude_dirs works when local_repo_path is provided."""
- config = {
- 'repo': 'owner/repo',
- 'local_repo_path': '/tmp/test/repo',
- 'exclude_dirs_additional': ['proprietary', 'internal']
- }
-
- scraper = GitHubScraper(config)
-
- # Should have both defaults and additional
- self.assertIn('venv', scraper.excluded_dirs)
- self.assertIn('proprietary', scraper.excluded_dirs)
- self.assertIn('internal', scraper.excluded_dirs)
-
- # Test exclusion works
- self.assertTrue(scraper.should_exclude_dir('proprietary'))
- self.assertTrue(scraper.should_exclude_dir('internal'))
- self.assertTrue(scraper.should_exclude_dir('venv'))
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_replace_mode_with_local_repo_path(self, mock_github):
- """Test that replace mode works with local_repo_path."""
- config = {
- 'repo': 'owner/repo',
- 'local_repo_path': '/tmp/test/repo',
- 'exclude_dirs': ['only_this']
- }
-
- scraper = GitHubScraper(config)
-
- # Should ONLY have specified dir
- self.assertEqual(scraper.excluded_dirs, {'only_this'})
- self.assertTrue(scraper.should_exclude_dir('only_this'))
- self.assertFalse(scraper.should_exclude_dir('venv'))
-
-
-class TestExcludedDirsLogging(unittest.TestCase):
- """Test logging output for exclude_dirs configuration."""
-
- @patch('skill_seekers.cli.github_scraper.Github')
- @patch('skill_seekers.cli.github_scraper.logger')
- def test_extend_mode_logs_info(self, mock_logger, mock_github):
- """Test that extend mode logs INFO level message."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs_additional': ['custom1', 'custom2']
- }
-
- scraper = GitHubScraper(config)
-
- # Should have logged INFO message
- # Check that info was called with a message about adding custom exclusions
- info_calls = [str(call) for call in mock_logger.info.call_args_list]
- self.assertTrue(any('Added 2 custom directory exclusions' in call for call in info_calls))
-
- @patch('skill_seekers.cli.github_scraper.Github')
- @patch('skill_seekers.cli.github_scraper.logger')
- def test_replace_mode_logs_warning(self, mock_logger, mock_github):
- """Test that replace mode logs WARNING level message."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs': ['only', 'these']
- }
-
- scraper = GitHubScraper(config)
-
- # Should have logged WARNING message
- warning_calls = [str(call) for call in mock_logger.warning.call_args_list]
- self.assertTrue(any('Using custom directory exclusions' in call and 'defaults overridden' in call for call in warning_calls))
-
- @patch('skill_seekers.cli.github_scraper.Github')
- @patch('skill_seekers.cli.github_scraper.logger')
- def test_no_config_no_logging(self, mock_logger, mock_github):
- """Test that default mode doesn't log exclude_dirs messages."""
- config = {
- 'repo': 'owner/repo'
- }
-
- scraper = GitHubScraper(config)
-
- # Should NOT have logged any exclude_dirs messages
- info_calls = [str(call) for call in mock_logger.info.call_args_list]
- warning_calls = [str(call) for call in mock_logger.warning.call_args_list]
-
- # Filter for exclude_dirs related messages
- exclude_info = [c for c in info_calls if 'directory exclusion' in c]
- exclude_warnings = [c for c in warning_calls if 'directory exclusion' in c]
-
- self.assertEqual(len(exclude_info), 0)
- self.assertEqual(len(exclude_warnings), 0)
-
-
-class TestExcludedDirsTypeHandling(unittest.TestCase):
- """Test type handling for exclude_dirs configuration."""
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_exclude_dirs_with_tuple(self, mock_github):
- """Test that tuples are converted to sets correctly."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs': ('node_modules', 'build') # Tuple instead of list
- }
-
- scraper = GitHubScraper(config)
-
- # Should work with tuples (set() accepts tuples)
- self.assertEqual(scraper.excluded_dirs, {'node_modules', 'build'})
-
- @patch('skill_seekers.cli.github_scraper.Github')
- def test_exclude_dirs_additional_with_set(self, mock_github):
- """Test that sets work correctly for exclude_dirs_additional."""
- config = {
- 'repo': 'owner/repo',
- 'exclude_dirs_additional': {'custom1', 'custom2'} # Set instead of list
- }
-
- scraper = GitHubScraper(config)
-
- # Should work with sets
- self.assertIn('custom1', scraper.excluded_dirs)
- self.assertIn('custom2', scraper.excluded_dirs)
- self.assertIn('venv', scraper.excluded_dirs) # Defaults still there
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_github_scraper.py b/assets/repo/Skill_Seekers-development/tests/test_github_scraper.py
deleted file mode 100644
index 463c84e..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_github_scraper.py
+++ /dev/null
@@ -1,731 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for GitHub Scraper (cli/github_scraper.py)
-
-Tests cover:
-- GitHubScraper initialization and configuration (C1.1)
-- README extraction (C1.2)
-- Language detection (C1.4)
-- GitHub Issues extraction (C1.7)
-- CHANGELOG extraction (C1.8)
-- GitHub Releases extraction (C1.9)
-- GitHubToSkillConverter and skill building (C1.10)
-- Authentication handling
-- Error handling and edge cases
-"""
-
-import unittest
-import sys
-import json
-import tempfile
-import shutil
-import os
-from pathlib import Path
-from unittest.mock import Mock, patch, MagicMock
-from datetime import datetime
-
-try:
- from github import Github, GithubException
- PYGITHUB_AVAILABLE = True
-except ImportError:
- PYGITHUB_AVAILABLE = False
-
-
-class TestGitHubScraperInitialization(unittest.TestCase):
- """Test GitHubScraper initialization and configuration (C1.1)"""
-
- def setUp(self):
- if not PYGITHUB_AVAILABLE:
- self.skipTest("PyGithub not installed")
- from skill_seekers.cli.github_scraper import GitHubScraper
- self.GitHubScraper = GitHubScraper
-
- # Create temporary directory for test output
- self.temp_dir = tempfile.mkdtemp()
- self.output_dir = Path(self.temp_dir)
-
- def tearDown(self):
- # Clean up temporary directory
- if hasattr(self, 'temp_dir'):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_init_with_repo_name(self):
- """Test initialization with repository name"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None
- }
-
- scraper = self.GitHubScraper(config)
-
- self.assertEqual(scraper.repo_name, 'facebook/react')
- self.assertEqual(scraper.name, 'react')
- self.assertIsNotNone(scraper.github)
-
- def test_init_with_token_from_config(self):
- """Test initialization with token from config"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': 'test_token_123'
- }
-
- with patch('skill_seekers.cli.github_scraper.Github') as mock_github:
- scraper = self.GitHubScraper(config)
- mock_github.assert_called_once_with('test_token_123')
-
- def test_init_with_token_from_env(self):
- """Test initialization with token from environment variable"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None
- }
-
- with patch.dict(os.environ, {'GITHUB_TOKEN': 'env_token_456'}):
- with patch('skill_seekers.cli.github_scraper.Github') as mock_github:
- scraper = self.GitHubScraper(config)
- mock_github.assert_called_once_with('env_token_456')
-
- def test_init_without_token(self):
- """Test initialization without authentication"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None
- }
-
- with patch('skill_seekers.cli.github_scraper.Github') as mock_github:
- with patch.dict(os.environ, {}, clear=True):
- scraper = self.GitHubScraper(config)
- # Should create unauthenticated client
- self.assertIsNotNone(scraper.github)
-
- def test_token_priority_env_over_config(self):
- """Test that GITHUB_TOKEN env var takes priority over config"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': 'config_token'
- }
-
- with patch.dict(os.environ, {'GITHUB_TOKEN': 'env_token'}):
- scraper = self.GitHubScraper(config)
- token = scraper._get_token()
- self.assertEqual(token, 'env_token')
-
-
-class TestREADMEExtraction(unittest.TestCase):
- """Test README extraction (C1.2)"""
-
- def setUp(self):
- if not PYGITHUB_AVAILABLE:
- self.skipTest("PyGithub not installed")
- from skill_seekers.cli.github_scraper import GitHubScraper
- self.GitHubScraper = GitHubScraper
-
- def test_extract_readme_success(self):
- """Test successful README extraction"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None
- }
-
- mock_content = Mock()
- mock_content.decoded_content = b'# React\n\nA JavaScript library'
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_contents.return_value = mock_content
-
- scraper._extract_readme()
-
- self.assertIn('readme', scraper.extracted_data)
- self.assertEqual(scraper.extracted_data['readme'], '# React\n\nA JavaScript library')
-
- def test_extract_readme_tries_multiple_locations(self):
- """Test that README extraction tries multiple file locations"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None
- }
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
-
- # Make first attempts fail, succeed on third
- def side_effect(path):
- if path in ['README.md', 'README.rst']:
- raise GithubException(404, 'Not found')
- mock_content = Mock()
- mock_content.decoded_content = b'# README'
- return mock_content
-
- scraper.repo.get_contents.side_effect = side_effect
-
- scraper._extract_readme()
-
- # Should have tried multiple paths
- self.assertGreaterEqual(scraper.repo.get_contents.call_count, 1)
-
- def test_extract_readme_not_found(self):
- """Test README extraction when no README exists"""
- config = {
- 'repo': 'test/norepo',
- 'name': 'norepo',
- 'github_token': None
- }
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_contents.side_effect = GithubException(404, 'Not found')
-
- scraper._extract_readme()
-
- # Should not crash, just log warning (readme initialized as empty string)
- self.assertEqual(scraper.extracted_data['readme'], '')
-
-
-class TestLanguageDetection(unittest.TestCase):
- """Test language detection (C1.4)"""
-
- def setUp(self):
- if not PYGITHUB_AVAILABLE:
- self.skipTest("PyGithub not installed")
- from skill_seekers.cli.github_scraper import GitHubScraper
- self.GitHubScraper = GitHubScraper
-
- def test_extract_languages_success(self):
- """Test successful language detection"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None
- }
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_languages.return_value = {
- 'JavaScript': 8000,
- 'TypeScript': 2000
- }
-
- scraper._extract_languages()
-
- self.assertIn('languages', scraper.extracted_data)
- self.assertIn('JavaScript', scraper.extracted_data['languages'])
- self.assertIn('TypeScript', scraper.extracted_data['languages'])
-
- # Check percentages
- js_data = scraper.extracted_data['languages']['JavaScript']
- self.assertEqual(js_data['bytes'], 8000)
- self.assertEqual(js_data['percentage'], 80.0)
-
- ts_data = scraper.extracted_data['languages']['TypeScript']
- self.assertEqual(ts_data['bytes'], 2000)
- self.assertEqual(ts_data['percentage'], 20.0)
-
- def test_extract_languages_empty(self):
- """Test language detection with no languages"""
- config = {
- 'repo': 'test/norepo',
- 'name': 'norepo',
- 'github_token': None
- }
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_languages.return_value = {}
-
- scraper._extract_languages()
-
- self.assertIn('languages', scraper.extracted_data)
- self.assertEqual(scraper.extracted_data['languages'], {})
-
-
-class TestIssuesExtraction(unittest.TestCase):
- """Test GitHub Issues extraction (C1.7)"""
-
- def setUp(self):
- if not PYGITHUB_AVAILABLE:
- self.skipTest("PyGithub not installed")
- from skill_seekers.cli.github_scraper import GitHubScraper
- self.GitHubScraper = GitHubScraper
-
- def test_extract_issues_success(self):
- """Test successful issues extraction"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None,
- 'max_issues': 10
- }
-
- # Create mock issues
- mock_label1 = Mock()
- mock_label1.name = 'bug'
- mock_label2 = Mock()
- mock_label2.name = 'high-priority'
-
- mock_milestone = Mock()
- mock_milestone.title = 'v18.0'
-
- mock_issue1 = Mock()
- mock_issue1.number = 123
- mock_issue1.title = 'Bug in useState'
- mock_issue1.state = 'open'
- mock_issue1.labels = [mock_label1, mock_label2]
- mock_issue1.milestone = mock_milestone
- mock_issue1.created_at = datetime(2023, 1, 1)
- mock_issue1.updated_at = datetime(2023, 1, 2)
- mock_issue1.closed_at = None
- mock_issue1.html_url = 'https://github.com/facebook/react/issues/123'
- mock_issue1.body = 'Issue description'
- mock_issue1.pull_request = None
-
- mock_label3 = Mock()
- mock_label3.name = 'enhancement'
-
- mock_issue2 = Mock()
- mock_issue2.number = 124
- mock_issue2.title = 'Feature request'
- mock_issue2.state = 'closed'
- mock_issue2.labels = [mock_label3]
- mock_issue2.milestone = None
- mock_issue2.created_at = datetime(2023, 1, 3)
- mock_issue2.updated_at = datetime(2023, 1, 4)
- mock_issue2.closed_at = datetime(2023, 1, 5)
- mock_issue2.html_url = 'https://github.com/facebook/react/issues/124'
- mock_issue2.body = 'Feature description'
- mock_issue2.pull_request = None
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_issues.return_value = [mock_issue1, mock_issue2]
-
- scraper._extract_issues()
-
- self.assertIn('issues', scraper.extracted_data)
- issues = scraper.extracted_data['issues']
- self.assertEqual(len(issues), 2)
-
- # Check first issue
- self.assertEqual(issues[0]['number'], 123)
- self.assertEqual(issues[0]['title'], 'Bug in useState')
- self.assertEqual(issues[0]['state'], 'open')
- self.assertEqual(issues[0]['labels'], ['bug', 'high-priority'])
- self.assertEqual(issues[0]['milestone'], 'v18.0')
-
- # Check second issue
- self.assertEqual(issues[1]['number'], 124)
- self.assertEqual(issues[1]['state'], 'closed')
- self.assertIsNone(issues[1]['milestone'])
-
- def test_extract_issues_filters_pull_requests(self):
- """Test that pull requests are filtered out from issues"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None,
- 'max_issues': 10
- }
-
- # Create mock issue (need all required attributes)
- mock_issue = Mock()
- mock_issue.number = 123
- mock_issue.title = 'Real issue'
- mock_issue.state = 'open'
- mock_issue.labels = []
- mock_issue.milestone = None
- mock_issue.created_at = datetime(2023, 1, 1)
- mock_issue.updated_at = datetime(2023, 1, 2)
- mock_issue.closed_at = None
- mock_issue.html_url = 'https://github.com/test/repo/issues/123'
- mock_issue.body = 'Issue body'
- mock_issue.pull_request = None
-
- mock_pr = Mock()
- mock_pr.number = 124
- mock_pr.title = 'Pull request'
- mock_pr.pull_request = Mock() # Has pull_request attribute
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_issues.return_value = [mock_issue, mock_pr]
-
- scraper._extract_issues()
-
- issues = scraper.extracted_data['issues']
- # Should only have the real issue, not the PR
- self.assertEqual(len(issues), 1)
- self.assertEqual(issues[0]['number'], 123)
-
- def test_extract_issues_respects_max_limit(self):
- """Test that max_issues limit is respected"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None,
- 'max_issues': 2
- }
-
- # Create 5 mock issues
- mock_issues = []
- for i in range(5):
- mock_issue = Mock()
- mock_issue.number = i
- mock_issue.title = f'Issue {i}'
- mock_issue.state = 'open'
- mock_issue.labels = []
- mock_issue.milestone = None
- mock_issue.created_at = datetime(2023, 1, 1)
- mock_issue.updated_at = datetime(2023, 1, 2)
- mock_issue.closed_at = None
- mock_issue.html_url = f'https://github.com/test/repo/issues/{i}'
- mock_issue.body = None
- mock_issue.pull_request = None
- mock_issues.append(mock_issue)
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_issues.return_value = mock_issues
-
- scraper._extract_issues()
-
- issues = scraper.extracted_data['issues']
- # Should only extract first 2 issues
- self.assertEqual(len(issues), 2)
-
-
-class TestChangelogExtraction(unittest.TestCase):
- """Test CHANGELOG extraction (C1.8)"""
-
- def setUp(self):
- if not PYGITHUB_AVAILABLE:
- self.skipTest("PyGithub not installed")
- from skill_seekers.cli.github_scraper import GitHubScraper
- self.GitHubScraper = GitHubScraper
-
- def test_extract_changelog_success(self):
- """Test successful CHANGELOG extraction"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None
- }
-
- mock_content = Mock()
- mock_content.decoded_content = b'# Changelog\n\n## v1.0.0\n- Initial release'
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_contents.return_value = mock_content
-
- scraper._extract_changelog()
-
- self.assertIn('changelog', scraper.extracted_data)
- self.assertIn('Initial release', scraper.extracted_data['changelog'])
-
- def test_extract_changelog_tries_multiple_locations(self):
- """Test that CHANGELOG extraction tries multiple file locations"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None
- }
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
-
- # Make first attempts fail
- call_count = {'count': 0}
-
- def side_effect(path):
- call_count['count'] += 1
- if path in ['CHANGELOG.md', 'CHANGES.md']:
- raise GithubException(404, 'Not found')
- mock_content = Mock()
- mock_content.decoded_content = b'# History'
- return mock_content
-
- scraper.repo.get_contents.side_effect = side_effect
-
- scraper._extract_changelog()
-
- # Should have tried multiple paths
- self.assertGreaterEqual(call_count['count'], 1)
-
- def test_extract_changelog_not_found(self):
- """Test CHANGELOG extraction when no changelog exists"""
- config = {
- 'repo': 'test/norepo',
- 'name': 'norepo',
- 'github_token': None
- }
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_contents.side_effect = GithubException(404, 'Not found')
-
- scraper._extract_changelog()
-
- # Should not crash, just log warning (changelog initialized as empty string)
- self.assertEqual(scraper.extracted_data['changelog'], '')
-
-
-class TestReleasesExtraction(unittest.TestCase):
- """Test GitHub Releases extraction (C1.9)"""
-
- def setUp(self):
- if not PYGITHUB_AVAILABLE:
- self.skipTest("PyGithub not installed")
- from skill_seekers.cli.github_scraper import GitHubScraper
- self.GitHubScraper = GitHubScraper
-
- def test_extract_releases_success(self):
- """Test successful releases extraction"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None
- }
-
- # Create mock releases
- mock_release1 = Mock()
- mock_release1.tag_name = 'v18.0.0'
- mock_release1.title = 'React 18.0.0'
- mock_release1.body = 'New features:\n- Concurrent rendering'
- mock_release1.draft = False
- mock_release1.prerelease = False
- mock_release1.created_at = datetime(2023, 3, 1)
- mock_release1.published_at = datetime(2023, 3, 1)
- mock_release1.html_url = 'https://github.com/facebook/react/releases/tag/v18.0.0'
- mock_release1.tarball_url = 'https://github.com/facebook/react/archive/v18.0.0.tar.gz'
- mock_release1.zipball_url = 'https://github.com/facebook/react/archive/v18.0.0.zip'
-
- mock_release2 = Mock()
- mock_release2.tag_name = 'v18.0.0-rc.0'
- mock_release2.title = 'React 18.0.0 RC'
- mock_release2.body = 'Release candidate'
- mock_release2.draft = False
- mock_release2.prerelease = True
- mock_release2.created_at = datetime(2023, 2, 1)
- mock_release2.published_at = datetime(2023, 2, 1)
- mock_release2.html_url = 'https://github.com/facebook/react/releases/tag/v18.0.0-rc.0'
- mock_release2.tarball_url = 'https://github.com/facebook/react/archive/v18.0.0-rc.0.tar.gz'
- mock_release2.zipball_url = 'https://github.com/facebook/react/archive/v18.0.0-rc.0.zip'
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_releases.return_value = [mock_release1, mock_release2]
-
- scraper._extract_releases()
-
- self.assertIn('releases', scraper.extracted_data)
- releases = scraper.extracted_data['releases']
- self.assertEqual(len(releases), 2)
-
- # Check first release
- self.assertEqual(releases[0]['tag_name'], 'v18.0.0')
- self.assertEqual(releases[0]['name'], 'React 18.0.0')
- self.assertFalse(releases[0]['draft'])
- self.assertFalse(releases[0]['prerelease'])
- self.assertIn('Concurrent rendering', releases[0]['body'])
-
- # Check second release (prerelease)
- self.assertEqual(releases[1]['tag_name'], 'v18.0.0-rc.0')
- self.assertTrue(releases[1]['prerelease'])
-
- def test_extract_releases_empty(self):
- """Test releases extraction with no releases"""
- config = {
- 'repo': 'test/norepo',
- 'name': 'norepo',
- 'github_token': None
- }
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_releases.return_value = []
-
- scraper._extract_releases()
-
- self.assertIn('releases', scraper.extracted_data)
- self.assertEqual(scraper.extracted_data['releases'], [])
-
-
-class TestGitHubToSkillConverter(unittest.TestCase):
- """Test GitHubToSkillConverter and skill building (C1.10)"""
-
- def setUp(self):
- if not PYGITHUB_AVAILABLE:
- self.skipTest("PyGithub not installed")
- from skill_seekers.cli.github_scraper import GitHubToSkillConverter
- self.GitHubToSkillConverter = GitHubToSkillConverter
-
- # Create temporary directory for test output
- self.temp_dir = tempfile.mkdtemp()
- self.output_dir = Path(self.temp_dir)
-
- # Create mock data file
- self.data_file = self.output_dir / "test_github_data.json"
- self.mock_data = {
- 'repo_info': {
- 'name': 'react',
- 'full_name': 'facebook/react',
- 'description': 'A JavaScript library',
- 'stars': 200000,
- 'language': 'JavaScript'
- },
- 'readme': '# React\n\nA JavaScript library for building user interfaces.',
- 'languages': {
- 'JavaScript': {'bytes': 8000, 'percentage': 80.0},
- 'TypeScript': {'bytes': 2000, 'percentage': 20.0}
- },
- 'issues': [
- {
- 'number': 123,
- 'title': 'Bug in useState',
- 'state': 'open',
- 'labels': ['bug'],
- 'milestone': 'v18.0',
- 'created_at': '2023-01-01T10:00:00',
- 'updated_at': '2023-01-02T10:00:00',
- 'closed_at': None,
- 'url': 'https://github.com/facebook/react/issues/123',
- 'body': 'Issue description'
- }
- ],
- 'changelog': '# Changelog\n\n## v18.0.0\n- New features',
- 'releases': [
- {
- 'tag_name': 'v18.0.0',
- 'name': 'React 18.0.0',
- 'body': 'Release notes',
- 'published_at': '2023-03-01T10:00:00',
- 'prerelease': False,
- 'draft': False,
- 'url': 'https://github.com/facebook/react/releases/tag/v18.0.0'
- }
- ]
- }
-
- with open(self.data_file, 'w') as f:
- json.dump(self.mock_data, f)
-
- def tearDown(self):
- # Clean up temporary directory
- if hasattr(self, 'temp_dir'):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_init_loads_data(self):
- """Test that converter loads data file on initialization"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'test',
- 'description': 'Test skill'
- }
-
- # Override data file path
- with patch('skill_seekers.cli.github_scraper.GitHubToSkillConverter.__init__') as mock_init:
- mock_init.return_value = None
- converter = self.GitHubToSkillConverter(config)
- converter.data_file = str(self.data_file)
- converter.data = converter._load_data()
-
- self.assertIn('repo_info', converter.data)
- self.assertEqual(converter.data['repo_info']['name'], 'react')
-
- def test_build_skill_creates_directory_structure(self):
- """Test that build_skill creates proper directory structure"""
- # Create data file in expected location
- data_file_path = self.output_dir / 'test_github_data.json'
- with open(data_file_path, 'w') as f:
- json.dump(self.mock_data, f)
-
- config = {
- 'repo': 'facebook/react',
- 'name': 'test',
- 'description': 'Test skill'
- }
-
- # Patch the paths to use our temp directory
- with patch('skill_seekers.cli.github_scraper.GitHubToSkillConverter._load_data') as mock_load:
- mock_load.return_value = self.mock_data
- converter = self.GitHubToSkillConverter(config)
- converter.skill_dir = str(self.output_dir / 'test_skill')
- converter.data = self.mock_data
-
- converter.build_skill()
-
- skill_dir = Path(converter.skill_dir)
- self.assertTrue(skill_dir.exists())
- self.assertTrue((skill_dir / 'SKILL.md').exists())
- self.assertTrue((skill_dir / 'references').exists())
-
-
-class TestErrorHandling(unittest.TestCase):
- """Test error handling and edge cases"""
-
- def setUp(self):
- if not PYGITHUB_AVAILABLE:
- self.skipTest("PyGithub not installed")
- from skill_seekers.cli.github_scraper import GitHubScraper
- self.GitHubScraper = GitHubScraper
-
- def test_invalid_repo_name(self):
- """Test handling of invalid repository name"""
- config = {
- 'repo': 'invalid_repo_format',
- 'name': 'test',
- 'github_token': None
- }
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = None
- scraper.github.get_repo = Mock(side_effect=GithubException(404, 'Not found'))
-
- # Should raise ValueError with helpful message
- with self.assertRaises(ValueError) as context:
- scraper._fetch_repository()
-
- self.assertIn('Repository not found', str(context.exception))
-
- def test_rate_limit_error(self):
- """Test handling of rate limit errors"""
- config = {
- 'repo': 'facebook/react',
- 'name': 'react',
- 'github_token': None,
- 'max_issues': 10
- }
-
- with patch('skill_seekers.cli.github_scraper.Github'):
- scraper = self.GitHubScraper(config)
- scraper.repo = Mock()
- scraper.repo.get_issues.side_effect = GithubException(403, 'Rate limit exceeded')
-
- # Should handle gracefully and log warning
- scraper._extract_issues()
- # Should not crash, just log warning
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_integration.py b/assets/repo/Skill_Seekers-development/tests/test_integration.py
deleted file mode 100644
index 41086d0..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_integration.py
+++ /dev/null
@@ -1,647 +0,0 @@
-#!/usr/bin/env python3
-"""
-Integration tests for doc_scraper
-Tests complete workflows and dry-run mode
-"""
-
-import sys
-import os
-import unittest
-import json
-import tempfile
-import shutil
-from pathlib import Path
-
-# Add parent directory to path
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.doc_scraper import DocToSkillConverter, load_config, validate_config
-
-
-class TestDryRunMode(unittest.TestCase):
- """Test dry-run mode functionality"""
-
- def setUp(self):
- """Set up test configuration"""
- self.config = {
- 'name': 'test-dry-run',
- 'base_url': 'https://example.com/',
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'url_patterns': {
- 'include': [],
- 'exclude': []
- },
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
-
- def test_dry_run_no_directories_created(self):
- """Test that dry-run mode doesn't create directories"""
- converter = DocToSkillConverter(self.config, dry_run=True)
-
- # Check directories were NOT created
- data_dir = Path(f"output/{self.config['name']}_data")
- skill_dir = Path(f"output/{self.config['name']}")
-
- self.assertFalse(data_dir.exists(), "Dry-run should not create data directory")
- self.assertFalse(skill_dir.exists(), "Dry-run should not create skill directory")
-
- def test_dry_run_flag_set(self):
- """Test that dry_run flag is properly set"""
- converter = DocToSkillConverter(self.config, dry_run=True)
- self.assertTrue(converter.dry_run)
-
- converter_normal = DocToSkillConverter(self.config, dry_run=False)
- self.assertFalse(converter_normal.dry_run)
-
- # Clean up
- shutil.rmtree(f"output/{self.config['name']}_data", ignore_errors=True)
- shutil.rmtree(f"output/{self.config['name']}", ignore_errors=True)
-
- def test_normal_mode_creates_directories(self):
- """Test that normal mode creates directories"""
- converter = DocToSkillConverter(self.config, dry_run=False)
-
- # Check directories WERE created
- data_dir = Path(f"output/{self.config['name']}_data")
- skill_dir = Path(f"output/{self.config['name']}")
-
- self.assertTrue(data_dir.exists(), "Normal mode should create data directory")
- self.assertTrue(skill_dir.exists(), "Normal mode should create skill directory")
-
- # Clean up
- shutil.rmtree(data_dir, ignore_errors=True)
- shutil.rmtree(skill_dir, ignore_errors=True)
-
-
-class TestConfigLoading(unittest.TestCase):
- """Test configuration loading and validation"""
-
- def setUp(self):
- """Set up temporary directory for test configs"""
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- """Clean up temporary directory"""
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_load_valid_config(self):
- """Test loading a valid configuration file"""
- config_data = {
- 'name': 'test-config',
- 'base_url': 'https://example.com/',
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'rate_limit': 0.5,
- 'max_pages': 100
- }
-
- config_path = Path(self.temp_dir) / 'test.json'
- with open(config_path, 'w') as f:
- json.dump(config_data, f)
-
- loaded_config = load_config(str(config_path))
- self.assertEqual(loaded_config['name'], 'test-config')
- self.assertEqual(loaded_config['base_url'], 'https://example.com/')
-
- def test_load_invalid_json(self):
- """Test loading an invalid JSON file"""
- config_path = Path(self.temp_dir) / 'invalid.json'
- with open(config_path, 'w') as f:
- f.write('{ invalid json }')
-
- with self.assertRaises(SystemExit):
- load_config(str(config_path))
-
- def test_load_nonexistent_file(self):
- """Test loading a nonexistent file"""
- config_path = Path(self.temp_dir) / 'nonexistent.json'
-
- with self.assertRaises(SystemExit):
- load_config(str(config_path))
-
- def test_load_config_with_validation_errors(self):
- """Test loading a config with validation errors"""
- config_data = {
- 'name': 'invalid@name', # Invalid name
- 'base_url': 'example.com' # Missing protocol
- }
-
- config_path = Path(self.temp_dir) / 'invalid_config.json'
- with open(config_path, 'w') as f:
- json.dump(config_data, f)
-
- with self.assertRaises(SystemExit):
- load_config(str(config_path))
-
-
-class TestRealConfigFiles(unittest.TestCase):
- """Test that real config files in the repository are valid"""
-
- def test_godot_config(self):
- """Test Godot config is valid"""
- config_path = 'configs/godot.json'
- if os.path.exists(config_path):
- config = load_config(config_path)
- errors, _ = validate_config(config)
- self.assertEqual(len(errors), 0, f"Godot config should be valid, got errors: {errors}")
-
- def test_react_config(self):
- """Test React config is valid"""
- config_path = 'configs/react.json'
- if os.path.exists(config_path):
- config = load_config(config_path)
- errors, _ = validate_config(config)
- self.assertEqual(len(errors), 0, f"React config should be valid, got errors: {errors}")
-
- def test_vue_config(self):
- """Test Vue config is valid"""
- config_path = 'configs/vue.json'
- if os.path.exists(config_path):
- config = load_config(config_path)
- errors, _ = validate_config(config)
- self.assertEqual(len(errors), 0, f"Vue config should be valid, got errors: {errors}")
-
- def test_django_config(self):
- """Test Django config is valid"""
- config_path = 'configs/django.json'
- if os.path.exists(config_path):
- config = load_config(config_path)
- errors, _ = validate_config(config)
- self.assertEqual(len(errors), 0, f"Django config should be valid, got errors: {errors}")
-
- def test_fastapi_config(self):
- """Test FastAPI config is valid"""
- config_path = 'configs/fastapi.json'
- if os.path.exists(config_path):
- config = load_config(config_path)
- errors, _ = validate_config(config)
- self.assertEqual(len(errors), 0, f"FastAPI config should be valid, got errors: {errors}")
-
- def test_steam_economy_config(self):
- """Test Steam Economy config is valid"""
- config_path = 'configs/steam-economy-complete.json'
- if os.path.exists(config_path):
- config = load_config(config_path)
- errors, _ = validate_config(config)
- self.assertEqual(len(errors), 0, f"Steam Economy config should be valid, got errors: {errors}")
-
-
-class TestURLProcessing(unittest.TestCase):
- """Test URL processing and validation"""
-
- def test_url_normalization(self):
- """Test URL normalization in converter"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article', 'title': 'h1', 'code_blocks': 'pre'},
- 'url_patterns': {'include': [], 'exclude': []},
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- converter = DocToSkillConverter(config, dry_run=True)
-
- # Base URL should be stored correctly
- self.assertEqual(converter.base_url, 'https://example.com/')
-
- def test_start_urls_fallback(self):
- """Test that start_urls defaults to base_url"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article', 'title': 'h1', 'code_blocks': 'pre'},
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- converter = DocToSkillConverter(config, dry_run=True)
-
- # Should have base_url in pending_urls
- self.assertEqual(len(converter.pending_urls), 1)
- self.assertEqual(converter.pending_urls[0], 'https://example.com/')
-
- def test_multiple_start_urls(self):
- """Test multiple start URLs"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'start_urls': [
- 'https://example.com/guide/',
- 'https://example.com/api/',
- 'https://example.com/tutorial/'
- ],
- 'selectors': {'main_content': 'article', 'title': 'h1', 'code_blocks': 'pre'},
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- converter = DocToSkillConverter(config, dry_run=True)
-
- # Should have all start URLs in pending_urls
- self.assertEqual(len(converter.pending_urls), 3)
-
-
-class TestLlmsTxtIntegration(unittest.TestCase):
- """Test llms.txt integration into scraping workflow"""
-
- def test_scraper_has_llms_txt_attributes(self):
- """Test that scraper has llms.txt detection attributes"""
- config = {
- 'name': 'test-llms',
- 'base_url': 'https://hono.dev/docs',
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'max_pages': 50
- }
-
- scraper = DocToSkillConverter(config, dry_run=True)
-
- # Should have llms.txt attributes
- self.assertFalse(scraper.llms_txt_detected)
- self.assertIsNone(scraper.llms_txt_variant)
-
- def test_scraper_has_try_llms_txt_method(self):
- """Test that scraper has _try_llms_txt method"""
- config = {
- 'name': 'test-llms',
- 'base_url': 'https://hono.dev/docs',
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'max_pages': 50
- }
-
- scraper = DocToSkillConverter(config, dry_run=True)
-
- # Should have _try_llms_txt method
- self.assertTrue(hasattr(scraper, '_try_llms_txt'))
- self.assertTrue(callable(getattr(scraper, '_try_llms_txt')))
-
-
-class TestContentExtraction(unittest.TestCase):
- """Test content extraction functionality"""
-
- def setUp(self):
- """Set up test converter"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- self.converter = DocToSkillConverter(config, dry_run=True)
-
- def test_extract_empty_content(self):
- """Test extracting from empty HTML"""
- from bs4 import BeautifulSoup
- html = ''
- soup = BeautifulSoup(html, 'html.parser')
-
- page = self.converter.extract_content(soup, 'https://example.com/test')
-
- self.assertEqual(page['url'], 'https://example.com/test')
- self.assertEqual(page['title'], '')
- self.assertEqual(page['content'], '')
- self.assertEqual(len(page['code_samples']), 0)
-
- def test_extract_basic_content(self):
- """Test extracting basic content"""
- from bs4 import BeautifulSoup
- html = '''
-
- Test Page
-
-
- Page Title
- This is some content.
- This is more content with sufficient length to be included.
- print("hello")
-
-
-
- '''
- soup = BeautifulSoup(html, 'html.parser')
-
- page = self.converter.extract_content(soup, 'https://example.com/test')
-
- self.assertEqual(page['url'], 'https://example.com/test')
- self.assertIn('Page Title', page['title'])
- self.assertIn('content', page['content'].lower())
- self.assertGreater(len(page['code_samples']), 0)
- self.assertEqual(page['code_samples'][0]['language'], 'python')
-
-
-class TestFullLlmsTxtWorkflow(unittest.TestCase):
- """Test complete llms.txt workflow with mocked HTTP requests"""
-
- def setUp(self):
- """Set up test configuration and temporary directory"""
- self.temp_dir = tempfile.mkdtemp()
- self.config = {
- 'name': 'test-e2e-llms',
- 'base_url': 'https://hono.dev/docs',
- 'llms_txt_url': 'https://hono.dev/llms-full.txt',
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'max_pages': 50
- }
-
- # Sample llms.txt content for testing
- self.sample_llms_content = """# Getting Started
-
-Welcome to the framework documentation. This is the introduction section.
-
-## Installation
-
-To install the framework, run the following command:
-
-```bash
-npm install hono
-```
-
-## Quick Start
-
-Create a simple application:
-
-```javascript
-import { Hono } from 'hono'
-
-const app = new Hono()
-
-app.get('/', (c) => {
- return c.text('Hello World!')
-})
-
-export default app
-```
-
-# API Reference
-
-This section covers the API documentation for the framework.
-
-## Context
-
-The context object provides request and response handling:
-
-```typescript
-interface Context {
- req: Request
- res: Response
- text: (text: string) => Response
-}
-```
-
-# Middleware
-
-Middleware functions run before route handlers.
-
-## Built-in Middleware
-
-The framework provides several built-in middleware functions:
-
-```javascript
-import { logger, cors } from 'hono/middleware'
-
-app.use('*', logger())
-app.use('*', cors())
-```
-"""
-
- def tearDown(self):
- """Clean up temporary directory and test output"""
- shutil.rmtree(self.temp_dir, ignore_errors=True)
- # Clean up test output directories
- shutil.rmtree(f"output/{self.config['name']}_data", ignore_errors=True)
- shutil.rmtree(f"output/{self.config['name']}", ignore_errors=True)
-
- def test_full_llms_txt_workflow(self):
- """Test complete workflow: config -> scrape (llms.txt) -> build -> verify"""
- from unittest.mock import patch, MagicMock
- import requests
-
- # Mock the requests.get call for downloading llms.txt
- with patch('cli.llms_txt_downloader.requests.get') as mock_get:
- # Configure mock response
- mock_response = MagicMock()
- mock_response.status_code = 200
- mock_response.text = self.sample_llms_content
- mock_response.raise_for_status = MagicMock()
- mock_get.return_value = mock_response
-
- # Create scraper and scrape
- scraper = DocToSkillConverter(self.config, dry_run=False)
- scraper.scrape_all()
-
- # Verify llms.txt was detected
- self.assertTrue(scraper.llms_txt_detected,
- "llms.txt should be detected")
- self.assertEqual(scraper.llms_txt_variant, 'explicit',
- "Should use explicit variant from config")
-
- # Verify pages were parsed
- self.assertGreater(len(scraper.pages), 0,
- "Should have parsed pages from llms.txt")
-
- # Verify page structure
- self.assertTrue(all('title' in page for page in scraper.pages),
- "All pages should have titles")
- self.assertTrue(all('content' in page for page in scraper.pages),
- "All pages should have content")
- self.assertTrue(any(len(page.get('code_samples', [])) > 0
- for page in scraper.pages),
- "At least one page should have code samples")
-
- # Verify code samples have language detection
- pages_with_code = [p for p in scraper.pages
- if len(p.get('code_samples', [])) > 0]
- if pages_with_code:
- sample = pages_with_code[0]['code_samples'][0]
- self.assertIn('language', sample,
- "Code samples should have language field")
- self.assertIn('code', sample,
- "Code samples should have code field")
-
- # Build skill
- scraper.build_skill()
-
- # Verify SKILL.md exists
- skill_md_path = Path(f"output/{self.config['name']}/SKILL.md")
- self.assertTrue(skill_md_path.exists(),
- "SKILL.md should be created")
-
- # Verify SKILL.md content
- skill_content = skill_md_path.read_text()
- self.assertIn(self.config['name'], skill_content,
- "SKILL.md should contain skill name")
- self.assertGreater(len(skill_content), 100,
- "SKILL.md should have substantial content")
-
- # Verify references directory exists
- refs_dir = Path(f"output/{self.config['name']}/references")
- self.assertTrue(refs_dir.exists(),
- "references directory should exist")
-
- # Verify at least index.md was created
- index_md = refs_dir / 'index.md'
- self.assertTrue(index_md.exists(),
- "references/index.md should exist")
-
- # Verify reference files have content
- ref_files = list(refs_dir.glob('*.md'))
- self.assertGreater(len(ref_files), 0,
- "Should have at least one reference file")
-
- # Verify data directory was created and has summary
- data_dir = Path(f"output/{self.config['name']}_data")
- self.assertTrue(data_dir.exists(),
- "Data directory should exist")
-
- summary_path = data_dir / 'summary.json'
- self.assertTrue(summary_path.exists(),
- "summary.json should exist")
-
- # Verify summary content
- with open(summary_path) as f:
- summary = json.load(f)
- self.assertEqual(summary['name'], self.config['name'])
- self.assertGreater(summary['total_pages'], 0)
- self.assertIn('llms_txt_detected', summary)
- self.assertTrue(summary['llms_txt_detected'])
-
- def test_multi_variant_download(self):
- """Test downloading all 3 llms.txt variants"""
- from unittest.mock import patch, Mock
-
- config = {
- 'name': 'test-multi-variant',
- 'base_url': 'https://hono.dev/docs',
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'max_pages': 50
- }
-
- # Mock all 3 variants
- sample_full = "# Full\n" + "x" * 1000
- sample_standard = "# Standard\n" + "x" * 200
- sample_small = "# Small\n" + "x" * 500
-
- with patch('cli.llms_txt_detector.requests.head') as mock_head, \
- patch('cli.llms_txt_downloader.requests.get') as mock_get:
-
- # Mock detection (all exist)
- mock_head_response = Mock()
- mock_head_response.status_code = 200
- mock_head.return_value = mock_head_response
-
- # Mock downloads
- def mock_download(url, **kwargs):
- response = Mock()
- response.status_code = 200
- if 'llms-full.txt' in url:
- response.text = sample_full
- elif 'llms-small.txt' in url:
- response.text = sample_small
- else: # llms.txt
- response.text = sample_standard
- response.raise_for_status = Mock()
- return response
-
- mock_get.side_effect = mock_download
-
- # Run scraper
- from skill_seekers.cli.doc_scraper import DocToSkillConverter as DocumentationScraper
- scraper = DocumentationScraper(config, dry_run=False)
- result = scraper._try_llms_txt()
-
- # Verify all 3 files created
- refs_dir = Path(f"output/{config['name']}/references")
-
- self.assertTrue(refs_dir.exists(), "references directory should exist")
- self.assertTrue((refs_dir / 'llms-full.md').exists(), "llms-full.md should exist")
- self.assertTrue((refs_dir / 'llms.md').exists(), "llms.md should exist")
- self.assertTrue((refs_dir / 'llms-small.md').exists(), "llms-small.md should exist")
-
- # Verify content not truncated
- full_content = (refs_dir / 'llms-full.md').read_text()
- self.assertEqual(len(full_content), len(sample_full))
-
- # Clean up
- shutil.rmtree(f"output/{config['name']}_data", ignore_errors=True)
- shutil.rmtree(f"output/{config['name']}", ignore_errors=True)
-
-def test_no_content_truncation():
- """Test that content is NOT truncated in reference files"""
- from unittest.mock import Mock
- import tempfile
-
- config = {
- 'name': 'test-no-truncate',
- 'base_url': 'https://example.com/docs',
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'max_pages': 50
- }
-
- # Create scraper with long content
- from skill_seekers.cli.doc_scraper import DocToSkillConverter
- scraper = DocToSkillConverter(config, dry_run=False)
-
- # Create page with content > 2500 chars
- long_content = "x" * 5000
- long_code = "y" * 1000
-
- pages = [{
- 'title': 'Long Page',
- 'url': 'https://example.com/long',
- 'content': long_content,
- 'code_samples': [
- {'code': long_code, 'language': 'python'}
- ],
- 'headings': []
- }]
-
- # Create reference file
- scraper.create_reference_file('test', pages)
-
- # Verify no truncation
- ref_file = Path(f"output/{config['name']}/references/test.md")
- with open(ref_file, 'r') as f:
- content = f.read()
-
- assert long_content in content # Full content included
- assert long_code in content # Full code included
- assert '[Content truncated]' not in content
- assert '...' not in content or content.count('...') == 0
-
- # Clean up
- shutil.rmtree(f"output/{config['name']}_data", ignore_errors=True)
- shutil.rmtree(f"output/{config['name']}", ignore_errors=True)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_llms_txt_detector.py b/assets/repo/Skill_Seekers-development/tests/test_llms_txt_detector.py
deleted file mode 100644
index 68c8b43..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_llms_txt_detector.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import pytest
-from unittest.mock import patch, Mock
-from skill_seekers.cli.llms_txt_detector import LlmsTxtDetector
-
-def test_detect_llms_txt_variants():
- """Test detection of llms.txt file variants"""
- detector = LlmsTxtDetector("https://hono.dev/docs")
-
- with patch('skill_seekers.cli.llms_txt_detector.requests.head') as mock_head:
- mock_response = Mock()
- mock_response.status_code = 200
- mock_head.return_value = mock_response
-
- variants = detector.detect()
-
- assert variants is not None
- assert variants['url'] == 'https://hono.dev/llms-full.txt'
- assert variants['variant'] == 'full'
- mock_head.assert_called()
-
-def test_detect_no_llms_txt():
- """Test detection when no llms.txt file exists"""
- detector = LlmsTxtDetector("https://example.com/docs")
-
- with patch('skill_seekers.cli.llms_txt_detector.requests.head') as mock_head:
- mock_response = Mock()
- mock_response.status_code = 404
- mock_head.return_value = mock_response
-
- variants = detector.detect()
-
- assert variants is None
- assert mock_head.call_count == 3 # Should try all three variants
-
-def test_url_parsing_with_complex_paths():
- """Test URL parsing handles non-standard paths correctly"""
- detector = LlmsTxtDetector("https://example.com/docs/v2/guide")
-
- with patch('skill_seekers.cli.llms_txt_detector.requests.head') as mock_head:
- mock_response = Mock()
- mock_response.status_code = 200
- mock_head.return_value = mock_response
-
- variants = detector.detect()
-
- assert variants is not None
- assert variants['url'] == 'https://example.com/llms-full.txt'
- mock_head.assert_called_with(
- 'https://example.com/llms-full.txt',
- timeout=5,
- allow_redirects=True
- )
-
-def test_detect_all_variants():
- """Test detecting all llms.txt variants"""
- detector = LlmsTxtDetector("https://hono.dev/docs")
-
- with patch('skill_seekers.cli.llms_txt_detector.requests.head') as mock_head:
- # Mock responses for different variants
- def mock_response(url, **kwargs):
- response = Mock()
- # All 3 variants exist for Hono
- if 'llms-full.txt' in url or 'llms.txt' in url or 'llms-small.txt' in url:
- response.status_code = 200
- else:
- response.status_code = 404
- return response
-
- mock_head.side_effect = mock_response
-
- variants = detector.detect_all()
-
- assert len(variants) == 3
- assert any(v['variant'] == 'full' for v in variants)
- assert any(v['variant'] == 'standard' for v in variants)
- assert any(v['variant'] == 'small' for v in variants)
- assert all('url' in v for v in variants)
diff --git a/assets/repo/Skill_Seekers-development/tests/test_llms_txt_downloader.py b/assets/repo/Skill_Seekers-development/tests/test_llms_txt_downloader.py
deleted file mode 100644
index 3b945fc..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_llms_txt_downloader.py
+++ /dev/null
@@ -1,170 +0,0 @@
-import pytest
-from unittest.mock import patch, Mock
-import requests
-from skill_seekers.cli.llms_txt_downloader import LlmsTxtDownloader
-
-def test_successful_download():
- """Test successful download with valid markdown content"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt")
-
- mock_response = Mock()
- mock_response.text = "# Header\n\nSome content with markdown patterns.\n\n## Subheader\n\n- List item\n- Another item\n\n```python\ncode_block()\n```\n" + "x" * 200
- mock_response.raise_for_status = Mock()
-
- with patch('requests.get', return_value=mock_response) as mock_get:
- content = downloader.download()
-
- assert content is not None
- assert len(content) > 100
- assert isinstance(content, str)
- assert "# Header" in content
- mock_get.assert_called_once()
-
-def test_timeout_with_retry():
- """Test timeout scenario with retry logic"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt", max_retries=2)
-
- with patch('requests.get', side_effect=requests.Timeout("Connection timeout")) as mock_get:
- with patch('time.sleep') as mock_sleep: # Mock sleep to speed up test
- content = downloader.download()
-
- assert content is None
- assert mock_get.call_count == 2 # Should retry once (2 total attempts)
- assert mock_sleep.call_count == 1 # Should sleep once between retries
-
-def test_empty_content_rejection():
- """Test rejection of content shorter than 100 chars"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt")
-
- mock_response = Mock()
- mock_response.text = "# Short"
- mock_response.raise_for_status = Mock()
-
- with patch('requests.get', return_value=mock_response):
- content = downloader.download()
-
- assert content is None
-
-def test_non_markdown_rejection():
- """Test rejection of content that doesn't look like markdown"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt")
-
- mock_response = Mock()
- mock_response.text = "Plain text without any markdown patterns at all. " * 10
- mock_response.raise_for_status = Mock()
-
- with patch('requests.get', return_value=mock_response):
- content = downloader.download()
-
- assert content is None
-
-def test_http_error_handling():
- """Test handling of HTTP errors (404, 500, etc.)"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt", max_retries=2)
-
- mock_response = Mock()
- mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
-
- with patch('requests.get', return_value=mock_response) as mock_get:
- with patch('time.sleep'):
- content = downloader.download()
-
- assert content is None
- assert mock_get.call_count == 2 # Should retry once
-
-def test_exponential_backoff():
- """Test that exponential backoff delays are correct"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt", max_retries=3)
-
- with patch('requests.get', side_effect=requests.Timeout("Connection timeout")):
- with patch('time.sleep') as mock_sleep:
- content = downloader.download()
-
- assert content is None
- # Should sleep with delays: 1s, 2s (2^0, 2^1)
- assert mock_sleep.call_count == 2
- mock_sleep.assert_any_call(1) # First retry delay
- mock_sleep.assert_any_call(2) # Second retry delay
-
-def test_markdown_validation():
- """Test markdown pattern detection"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt")
-
- # Test various markdown patterns
- assert downloader._is_markdown("# Header")
- assert downloader._is_markdown("## Subheader")
- assert downloader._is_markdown("```code```")
- assert downloader._is_markdown("- list item")
- assert downloader._is_markdown("* bullet point")
- assert downloader._is_markdown("`inline code`")
-
- # Test non-markdown content
- assert not downloader._is_markdown("Plain text without any markdown patterns")
-
-def test_custom_timeout():
- """Test custom timeout parameter"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt", timeout=10)
-
- mock_response = Mock()
- mock_response.text = "# Header\n\nContent " * 50
- mock_response.raise_for_status = Mock()
-
- with patch('requests.get', return_value=mock_response) as mock_get:
- content = downloader.download()
-
- assert content is not None
- # Verify timeout was passed to requests.get
- call_kwargs = mock_get.call_args[1]
- assert call_kwargs['timeout'] == 10
-
-def test_custom_max_retries():
- """Test custom max_retries parameter"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt", max_retries=5)
-
- with patch('requests.get', side_effect=requests.Timeout("Connection timeout")) as mock_get:
- with patch('time.sleep'):
- content = downloader.download()
-
- assert content is None
- assert mock_get.call_count == 5 # Should attempt 5 times
-
-def test_user_agent_header():
- """Test that custom user agent is set"""
- downloader = LlmsTxtDownloader("https://example.com/llms.txt")
-
- mock_response = Mock()
- mock_response.text = "# Header\n\nContent " * 50
- mock_response.raise_for_status = Mock()
-
- with patch('requests.get', return_value=mock_response) as mock_get:
- content = downloader.download()
-
- assert content is not None
- # Verify custom user agent was passed
- call_kwargs = mock_get.call_args[1]
- assert call_kwargs['headers']['User-Agent'] == 'Skill-Seekers-llms.txt-Reader/1.0'
-
-def test_get_proper_filename():
- """Test filename conversion from .txt to .md"""
- downloader = LlmsTxtDownloader("https://hono.dev/llms-full.txt")
-
- filename = downloader.get_proper_filename()
-
- assert filename == "llms-full.md"
- assert not filename.endswith('.txt')
-
-def test_get_proper_filename_standard():
- """Test standard variant naming"""
- downloader = LlmsTxtDownloader("https://hono.dev/llms.txt")
-
- filename = downloader.get_proper_filename()
-
- assert filename == "llms.md"
-
-def test_get_proper_filename_small():
- """Test small variant naming"""
- downloader = LlmsTxtDownloader("https://hono.dev/llms-small.txt")
-
- filename = downloader.get_proper_filename()
-
- assert filename == "llms-small.md"
diff --git a/assets/repo/Skill_Seekers-development/tests/test_llms_txt_parser.py b/assets/repo/Skill_Seekers-development/tests/test_llms_txt_parser.py
deleted file mode 100644
index 0a28bcb..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_llms_txt_parser.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import pytest
-from skill_seekers.cli.llms_txt_parser import LlmsTxtParser
-
-def test_parse_markdown_sections():
- """Test parsing markdown into page sections"""
- sample_content = """# Getting Started
-
-Welcome to the docs.
-
-## Installation
-
-Run: npm install
-
-## Usage
-
-Import the library:
-
-```javascript
-import { app } from 'framework'
-```
-
-# API Reference
-
-Main API documentation here.
-"""
-
- parser = LlmsTxtParser(sample_content)
- pages = parser.parse()
-
- assert len(pages) >= 2
- assert pages[0]['title'] == 'Getting Started'
- assert pages[1]['title'] == 'API Reference'
- assert len(pages[0]['code_samples']) == 1
- assert pages[0]['code_samples'][0]['language'] == 'javascript'
diff --git a/assets/repo/Skill_Seekers-development/tests/test_mcp_server.py b/assets/repo/Skill_Seekers-development/tests/test_mcp_server.py
deleted file mode 100644
index 421cb56..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_mcp_server.py
+++ /dev/null
@@ -1,618 +0,0 @@
-#!/usr/bin/env python3
-"""
-Comprehensive test suite for Skill Seeker MCP Server
-Tests all MCP tools and server functionality
-"""
-
-import sys
-import os
-import unittest
-import json
-import tempfile
-import shutil
-import asyncio
-from pathlib import Path
-from unittest.mock import Mock, patch, AsyncMock, MagicMock
-
-# CRITICAL: Import MCP package BEFORE adding project to path
-# to avoid shadowing the installed mcp package with our local mcp/ directory
-
-# WORKAROUND for shadowing issue: Temporarily change to /tmp to import external mcp
-# This avoids our local mcp/ directory being in the import path
-_original_dir = os.getcwd()
-try:
- os.chdir('/tmp') # Change away from project directory
- from mcp.server import Server
- from mcp.types import Tool, TextContent
- MCP_AVAILABLE = True
-except ImportError:
- MCP_AVAILABLE = False
- print("Warning: MCP package not available, skipping MCP tests")
-finally:
- os.chdir(_original_dir) # Restore original directory
-
-# NOW add parent directory to path for importing our local modules
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-# Import our local MCP server module
-if MCP_AVAILABLE:
- # Import from installed package (new src/ layout)
- try:
- from skill_seekers.mcp import server as skill_seeker_server
- except ImportError as e:
- print(f"Warning: Could not import skill_seeker server: {e}")
- skill_seeker_server = None
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestMCPServerInitialization(unittest.TestCase):
- """Test MCP server initialization"""
-
- def test_server_import(self):
- """Test that server module can be imported"""
- from mcp import server as mcp_server_module
- self.assertIsNotNone(mcp_server_module)
-
- def test_server_initialization(self):
- """Test server initializes correctly"""
- import mcp.server
- app = mcp.server.Server("test-skill-seeker")
- self.assertEqual(app.name, "test-skill-seeker")
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestListTools(unittest.IsolatedAsyncioTestCase):
- """Test list_tools functionality"""
-
- async def test_list_tools_returns_tools(self):
- """Test that list_tools returns all expected tools"""
- tools = await skill_seeker_server.list_tools()
-
- self.assertIsInstance(tools, list)
- self.assertGreater(len(tools), 0)
-
- # Check all expected tools are present
- tool_names = [tool.name for tool in tools]
- expected_tools = [
- "generate_config",
- "estimate_pages",
- "scrape_docs",
- "package_skill",
- "list_configs",
- "validate_config"
- ]
-
- for expected in expected_tools:
- self.assertIn(expected, tool_names, f"Missing tool: {expected}")
-
- async def test_tool_schemas(self):
- """Test that all tools have valid schemas"""
- tools = await skill_seeker_server.list_tools()
-
- for tool in tools:
- self.assertIsInstance(tool.name, str)
- self.assertIsInstance(tool.description, str)
- self.assertIn("inputSchema", tool.__dict__)
-
- # Verify schema has required structure
- schema = tool.inputSchema
- self.assertEqual(schema["type"], "object")
- self.assertIn("properties", schema)
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestGenerateConfigTool(unittest.IsolatedAsyncioTestCase):
- """Test generate_config tool"""
-
- async def asyncSetUp(self):
- """Set up test environment"""
- self.temp_dir = tempfile.mkdtemp()
- self.original_cwd = os.getcwd()
- os.chdir(self.temp_dir)
-
- async def asyncTearDown(self):
- """Clean up test environment"""
- os.chdir(self.original_cwd)
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- async def test_generate_config_basic(self):
- """Test basic config generation"""
- args = {
- "name": "test-framework",
- "url": "https://test-framework.dev/",
- "description": "Test framework skill"
- }
-
- result = await skill_seeker_server.generate_config_tool(args)
-
- self.assertIsInstance(result, list)
- self.assertGreater(len(result), 0)
- self.assertIsInstance(result[0], TextContent)
- self.assertIn("✅", result[0].text)
-
- # Verify config file was created
- config_path = Path("configs/test-framework.json")
- self.assertTrue(config_path.exists())
-
- # Verify config content
- with open(config_path) as f:
- config = json.load(f)
- self.assertEqual(config["name"], "test-framework")
- self.assertEqual(config["base_url"], "https://test-framework.dev/")
- self.assertEqual(config["description"], "Test framework skill")
-
- async def test_generate_config_with_options(self):
- """Test config generation with custom options"""
- args = {
- "name": "custom-framework",
- "url": "https://custom.dev/",
- "description": "Custom skill",
- "max_pages": 200,
- "rate_limit": 1.0
- }
-
- result = await skill_seeker_server.generate_config_tool(args)
-
- # Verify config has custom options
- config_path = Path("configs/custom-framework.json")
- with open(config_path) as f:
- config = json.load(f)
- self.assertEqual(config["max_pages"], 200)
- self.assertEqual(config["rate_limit"], 1.0)
-
- async def test_generate_config_defaults(self):
- """Test that default values are applied correctly"""
- args = {
- "name": "default-test",
- "url": "https://test.dev/",
- "description": "Test defaults"
- }
-
- result = await skill_seeker_server.generate_config_tool(args)
-
- config_path = Path("configs/default-test.json")
- with open(config_path) as f:
- config = json.load(f)
- self.assertEqual(config["max_pages"], 100) # Default
- self.assertEqual(config["rate_limit"], 0.5) # Default
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestEstimatePagesTool(unittest.IsolatedAsyncioTestCase):
- """Test estimate_pages tool"""
-
- async def asyncSetUp(self):
- """Set up test environment"""
- self.temp_dir = tempfile.mkdtemp()
- self.original_cwd = os.getcwd()
- os.chdir(self.temp_dir)
-
- # Create a test config
- os.makedirs("configs", exist_ok=True)
- self.config_path = Path("configs/test.json")
- config_data = {
- "name": "test",
- "base_url": "https://example.com/",
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre"
- },
- "rate_limit": 0.5,
- "max_pages": 50
- }
- with open(self.config_path, 'w') as f:
- json.dump(config_data, f)
-
- async def asyncTearDown(self):
- """Clean up test environment"""
- os.chdir(self.original_cwd)
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- @patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
- async def test_estimate_pages_success(self, mock_streaming):
- """Test successful page estimation"""
- # Mock successful subprocess run with streaming
- # Returns (stdout, stderr, returncode)
- mock_streaming.return_value = ("Estimated 50 pages", "", 0)
-
- args = {
- "config_path": str(self.config_path)
- }
-
- result = await skill_seeker_server.estimate_pages_tool(args)
-
- self.assertIsInstance(result, list)
- self.assertIsInstance(result[0], TextContent)
- self.assertIn("50 pages", result[0].text)
- # Should also have progress message
- self.assertIn("Estimating page count", result[0].text)
-
- @patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
- async def test_estimate_pages_with_max_discovery(self, mock_streaming):
- """Test page estimation with custom max_discovery"""
- # Mock successful subprocess run with streaming
- mock_streaming.return_value = ("Estimated 100 pages", "", 0)
-
- args = {
- "config_path": str(self.config_path),
- "max_discovery": 500
- }
-
- result = await skill_seeker_server.estimate_pages_tool(args)
-
- # Verify subprocess was called with correct args
- mock_streaming.assert_called_once()
- call_args = mock_streaming.call_args[0][0]
- self.assertIn("--max-discovery", call_args)
- self.assertIn("500", call_args)
-
- @patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
- async def test_estimate_pages_error(self, mock_streaming):
- """Test error handling in page estimation"""
- # Mock failed subprocess run with streaming
- mock_streaming.return_value = ("", "Config file not found", 1)
-
- args = {
- "config_path": "nonexistent.json"
- }
-
- result = await skill_seeker_server.estimate_pages_tool(args)
-
- self.assertIn("Error", result[0].text)
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestScrapeDocsTool(unittest.IsolatedAsyncioTestCase):
- """Test scrape_docs tool"""
-
- async def asyncSetUp(self):
- """Set up test environment"""
- self.temp_dir = tempfile.mkdtemp()
- self.original_cwd = os.getcwd()
- os.chdir(self.temp_dir)
-
- # Create test config
- os.makedirs("configs", exist_ok=True)
- self.config_path = Path("configs/test.json")
- config_data = {
- "name": "test",
- "base_url": "https://example.com/",
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre"
- }
- }
- with open(self.config_path, 'w') as f:
- json.dump(config_data, f)
-
- async def asyncTearDown(self):
- """Clean up test environment"""
- os.chdir(self.original_cwd)
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- @patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
- async def test_scrape_docs_basic(self, mock_streaming):
- """Test basic documentation scraping"""
- # Mock successful subprocess run with streaming
- mock_streaming.return_value = ("Scraping completed successfully", "", 0)
-
- args = {
- "config_path": str(self.config_path)
- }
-
- result = await skill_seeker_server.scrape_docs_tool(args)
-
- self.assertIsInstance(result, list)
- self.assertIn("success", result[0].text.lower())
-
- @patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
- async def test_scrape_docs_with_skip_scrape(self, mock_streaming):
- """Test scraping with skip_scrape flag"""
- # Mock successful subprocess run with streaming
- mock_streaming.return_value = ("Using cached data", "", 0)
-
- args = {
- "config_path": str(self.config_path),
- "skip_scrape": True
- }
-
- result = await skill_seeker_server.scrape_docs_tool(args)
-
- # Verify --skip-scrape was passed
- call_args = mock_streaming.call_args[0][0]
- self.assertIn("--skip-scrape", call_args)
-
- @patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
- async def test_scrape_docs_with_dry_run(self, mock_streaming):
- """Test scraping with dry_run flag"""
- # Mock successful subprocess run with streaming
- mock_streaming.return_value = ("Dry run completed", "", 0)
-
- args = {
- "config_path": str(self.config_path),
- "dry_run": True
- }
-
- result = await skill_seeker_server.scrape_docs_tool(args)
-
- call_args = mock_streaming.call_args[0][0]
- self.assertIn("--dry-run", call_args)
-
- @patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
- async def test_scrape_docs_with_enhance_local(self, mock_streaming):
- """Test scraping with local enhancement"""
- # Mock successful subprocess run with streaming
- mock_streaming.return_value = ("Scraping with enhancement", "", 0)
-
- args = {
- "config_path": str(self.config_path),
- "enhance_local": True
- }
-
- result = await skill_seeker_server.scrape_docs_tool(args)
-
- call_args = mock_streaming.call_args[0][0]
- self.assertIn("--enhance-local", call_args)
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestPackageSkillTool(unittest.IsolatedAsyncioTestCase):
- """Test package_skill tool"""
-
- async def asyncSetUp(self):
- """Set up test environment"""
- self.temp_dir = tempfile.mkdtemp()
- self.original_cwd = os.getcwd()
- os.chdir(self.temp_dir)
-
- # Create a mock skill directory
- self.skill_dir = Path("output/test-skill")
- self.skill_dir.mkdir(parents=True)
- (self.skill_dir / "SKILL.md").write_text("# Test Skill")
- (self.skill_dir / "references").mkdir()
- (self.skill_dir / "references/index.md").write_text("# Index")
-
- async def asyncTearDown(self):
- """Clean up test environment"""
- os.chdir(self.original_cwd)
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- @patch('subprocess.run')
- async def test_package_skill_success(self, mock_run):
- """Test successful skill packaging"""
- mock_result = MagicMock()
- mock_result.returncode = 0
- mock_result.stdout = "Package created: test-skill.zip"
- mock_run.return_value = mock_result
-
- args = {
- "skill_dir": str(self.skill_dir)
- }
-
- result = await skill_seeker_server.package_skill_tool(args)
-
- self.assertIsInstance(result, list)
- self.assertIn("test-skill", result[0].text)
-
- @patch('subprocess.run')
- async def test_package_skill_error(self, mock_run):
- """Test error handling in skill packaging"""
- mock_result = MagicMock()
- mock_result.returncode = 1
- mock_result.stderr = "Directory not found"
- mock_run.return_value = mock_result
-
- args = {
- "skill_dir": "nonexistent-dir"
- }
-
- result = await skill_seeker_server.package_skill_tool(args)
-
- self.assertIn("Error", result[0].text)
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestListConfigsTool(unittest.IsolatedAsyncioTestCase):
- """Test list_configs tool"""
-
- async def asyncSetUp(self):
- """Set up test environment"""
- self.temp_dir = tempfile.mkdtemp()
- self.original_cwd = os.getcwd()
- os.chdir(self.temp_dir)
-
- # Create test configs
- os.makedirs("configs", exist_ok=True)
-
- configs = [
- {
- "name": "test1",
- "description": "Test 1 skill",
- "base_url": "https://test1.dev/"
- },
- {
- "name": "test2",
- "description": "Test 2 skill",
- "base_url": "https://test2.dev/"
- }
- ]
-
- for config in configs:
- path = Path(f"configs/{config['name']}.json")
- with open(path, 'w') as f:
- json.dump(config, f)
-
- async def asyncTearDown(self):
- """Clean up test environment"""
- os.chdir(self.original_cwd)
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- async def test_list_configs_success(self):
- """Test listing all configs"""
- result = await skill_seeker_server.list_configs_tool({})
-
- self.assertIsInstance(result, list)
- self.assertIsInstance(result[0], TextContent)
- self.assertIn("test1", result[0].text)
- self.assertIn("test2", result[0].text)
- self.assertIn("https://test1.dev/", result[0].text)
- self.assertIn("https://test2.dev/", result[0].text)
-
- async def test_list_configs_empty(self):
- """Test listing configs when directory is empty"""
- # Remove all configs
- for config_file in Path("configs").glob("*.json"):
- config_file.unlink()
-
- result = await skill_seeker_server.list_configs_tool({})
-
- self.assertIn("No config files found", result[0].text)
-
- async def test_list_configs_no_directory(self):
- """Test listing configs when directory doesn't exist"""
- # Remove configs directory
- shutil.rmtree("configs")
-
- result = await skill_seeker_server.list_configs_tool({})
-
- self.assertIn("No configs directory", result[0].text)
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestValidateConfigTool(unittest.IsolatedAsyncioTestCase):
- """Test validate_config tool"""
-
- async def asyncSetUp(self):
- """Set up test environment"""
- self.temp_dir = tempfile.mkdtemp()
- self.original_cwd = os.getcwd()
- os.chdir(self.temp_dir)
-
- os.makedirs("configs", exist_ok=True)
-
- async def asyncTearDown(self):
- """Clean up test environment"""
- os.chdir(self.original_cwd)
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- async def test_validate_valid_config(self):
- """Test validating a valid config"""
- # Create valid config
- config_path = Path("configs/valid.json")
- valid_config = {
- "name": "valid-test",
- "base_url": "https://example.com/",
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre"
- },
- "rate_limit": 0.5,
- "max_pages": 100
- }
- with open(config_path, 'w') as f:
- json.dump(valid_config, f)
-
- args = {
- "config_path": str(config_path)
- }
-
- result = await skill_seeker_server.validate_config_tool(args)
-
- self.assertIsInstance(result, list)
- self.assertIn("✅", result[0].text)
- self.assertIn("valid", result[0].text.lower())
-
- async def test_validate_invalid_config(self):
- """Test validating an invalid config"""
- # Create invalid config (missing required fields)
- config_path = Path("configs/invalid.json")
- invalid_config = {
- "description": "Missing name field",
- "sources": [
- {"type": "invalid_type", "url": "https://example.com"} # Invalid source type
- ]
- }
- with open(config_path, 'w') as f:
- json.dump(invalid_config, f)
-
- args = {
- "config_path": str(config_path)
- }
-
- result = await skill_seeker_server.validate_config_tool(args)
-
- # Should show error for invalid source type
- self.assertIn("❌", result[0].text)
-
- async def test_validate_nonexistent_config(self):
- """Test validating a nonexistent config"""
- args = {
- "config_path": "configs/nonexistent.json"
- }
-
- result = await skill_seeker_server.validate_config_tool(args)
-
- self.assertIn("Error", result[0].text)
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestCallToolRouter(unittest.IsolatedAsyncioTestCase):
- """Test call_tool routing"""
-
- async def test_call_tool_unknown(self):
- """Test calling an unknown tool"""
- result = await skill_seeker_server.call_tool("unknown_tool", {})
-
- self.assertIsInstance(result, list)
- self.assertIn("Unknown tool", result[0].text)
-
- async def test_call_tool_exception_handling(self):
- """Test that exceptions are caught and returned as errors"""
- # Call with invalid arguments that should cause an exception
- result = await skill_seeker_server.call_tool("generate_config", {})
-
- self.assertIsInstance(result, list)
- self.assertIn("Error", result[0].text)
-
-
-@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed")
-class TestMCPServerIntegration(unittest.IsolatedAsyncioTestCase):
- """Integration tests for MCP server"""
-
- async def test_full_workflow_simulation(self):
- """Test complete workflow: generate config -> validate -> estimate"""
- temp_dir = tempfile.mkdtemp()
- original_cwd = os.getcwd()
- os.chdir(temp_dir)
-
- try:
- # Step 1: Generate config using skill_seeker_server
- generate_args = {
- "name": "workflow-test",
- "url": "https://workflow-test.dev/",
- "description": "Workflow test skill"
- }
- result1 = await skill_seeker_server.generate_config_tool(generate_args)
- self.assertIn("✅", result1[0].text)
-
- # Step 2: Validate config
- validate_args = {
- "config_path": "configs/workflow-test.json"
- }
- result2 = await skill_seeker_server.validate_config_tool(validate_args)
- self.assertIn("✅", result2[0].text)
-
- # Step 3: List configs
- result3 = await skill_seeker_server.list_configs_tool({})
- self.assertIn("workflow-test", result3[0].text)
-
- finally:
- os.chdir(original_cwd)
- shutil.rmtree(temp_dir, ignore_errors=True)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_package_skill.py b/assets/repo/Skill_Seekers-development/tests/test_package_skill.py
deleted file mode 100644
index b05b3c2..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_package_skill.py
+++ /dev/null
@@ -1,186 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for cli/package_skill.py functionality
-"""
-
-import unittest
-import tempfile
-import zipfile
-from pathlib import Path
-import sys
-
-from skill_seekers.cli.package_skill import package_skill
-
-
-class TestPackageSkill(unittest.TestCase):
- """Test package_skill function"""
-
- def create_test_skill_directory(self, tmpdir):
- """Helper to create a test skill directory structure"""
- skill_dir = Path(tmpdir) / "test-skill"
- skill_dir.mkdir()
-
- # Create SKILL.md
- (skill_dir / "SKILL.md").write_text("---\nname: test-skill\n---\n# Test Skill")
-
- # Create references directory
- refs_dir = skill_dir / "references"
- refs_dir.mkdir()
- (refs_dir / "index.md").write_text("# Index")
- (refs_dir / "getting_started.md").write_text("# Getting Started")
-
- # Create scripts directory (empty)
- (skill_dir / "scripts").mkdir()
-
- # Create assets directory (empty)
- (skill_dir / "assets").mkdir()
-
- return skill_dir
-
- def test_package_valid_skill_directory(self):
- """Test packaging a valid skill directory"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = self.create_test_skill_directory(tmpdir)
-
- success, zip_path = package_skill(skill_dir, open_folder_after=False, skip_quality_check=True)
-
- self.assertTrue(success)
- self.assertIsNotNone(zip_path)
- self.assertTrue(zip_path.exists())
- self.assertEqual(zip_path.suffix, '.zip')
- self.assertTrue(zipfile.is_zipfile(zip_path))
-
- def test_package_creates_correct_zip_structure(self):
- """Test that packaged zip contains correct files"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = self.create_test_skill_directory(tmpdir)
-
- success, zip_path = package_skill(skill_dir, open_folder_after=False, skip_quality_check=True)
-
- self.assertTrue(success)
-
- # Check zip contents
- with zipfile.ZipFile(zip_path, 'r') as zf:
- names = zf.namelist()
-
- # Should contain SKILL.md
- self.assertTrue(any('SKILL.md' in name for name in names))
-
- # Should contain references
- self.assertTrue(any('references/index.md' in name for name in names))
- self.assertTrue(any('references/getting_started.md' in name for name in names))
-
- def test_package_excludes_backup_files(self):
- """Test that .backup files are excluded from zip"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = self.create_test_skill_directory(tmpdir)
-
- # Add a backup file
- (skill_dir / "SKILL.md.backup").write_text("# Backup")
-
- success, zip_path = package_skill(skill_dir, open_folder_after=False, skip_quality_check=True)
-
- self.assertTrue(success)
-
- # Check that backup is NOT in zip
- with zipfile.ZipFile(zip_path, 'r') as zf:
- names = zf.namelist()
- self.assertFalse(any('.backup' in name for name in names))
-
- def test_package_nonexistent_directory(self):
- """Test packaging a nonexistent directory"""
- success, zip_path = package_skill("/nonexistent/path", open_folder_after=False, skip_quality_check=True)
-
- self.assertFalse(success)
- self.assertIsNone(zip_path)
-
- def test_package_directory_without_skill_md(self):
- """Test packaging directory without SKILL.md"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = Path(tmpdir) / "invalid-skill"
- skill_dir.mkdir()
-
- success, zip_path = package_skill(skill_dir, open_folder_after=False, skip_quality_check=True)
-
- self.assertFalse(success)
- self.assertIsNone(zip_path)
-
- def test_package_creates_zip_in_correct_location(self):
- """Test that zip is created in output/ directory"""
- with tempfile.TemporaryDirectory() as tmpdir:
- # Create skill in output-like structure
- output_dir = Path(tmpdir) / "output"
- output_dir.mkdir()
-
- skill_dir = output_dir / "test-skill"
- skill_dir.mkdir()
- (skill_dir / "SKILL.md").write_text("# Test")
- (skill_dir / "references").mkdir()
- (skill_dir / "scripts").mkdir()
- (skill_dir / "assets").mkdir()
-
- success, zip_path = package_skill(skill_dir, open_folder_after=False, skip_quality_check=True)
-
- self.assertTrue(success)
- # Zip should be in output directory, not inside skill directory
- self.assertEqual(zip_path.parent, output_dir)
- self.assertEqual(zip_path.name, "test-skill.zip")
-
- def test_package_zip_name_matches_skill_name(self):
- """Test that zip filename matches skill directory name"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = Path(tmpdir) / "my-awesome-skill"
- skill_dir.mkdir()
- (skill_dir / "SKILL.md").write_text("# Test")
- (skill_dir / "references").mkdir()
- (skill_dir / "scripts").mkdir()
- (skill_dir / "assets").mkdir()
-
- success, zip_path = package_skill(skill_dir, open_folder_after=False, skip_quality_check=True)
-
- self.assertTrue(success)
- self.assertEqual(zip_path.name, "my-awesome-skill.zip")
-
-
-class TestPackageSkillCLI(unittest.TestCase):
- """Test package_skill.py command-line interface"""
-
- def test_cli_help_output(self):
- """Test that skill-seekers package --help works"""
- import subprocess
-
- try:
- result = subprocess.run(
- ['skill-seekers', 'package', '--help'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # argparse may return 0 or 2 for --help
- self.assertIn(result.returncode, [0, 2])
- output = result.stdout + result.stderr
- self.assertTrue('usage:' in output.lower() or 'package' in output.lower())
- except FileNotFoundError:
- self.skipTest("skill-seekers command not installed")
-
- def test_cli_executes_without_errors(self):
- """Test that skill-seekers-package entry point works"""
- import subprocess
-
- try:
- result = subprocess.run(
- ['skill-seekers-package', '--help'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # argparse may return 0 or 2 for --help
- self.assertIn(result.returncode, [0, 2])
- except FileNotFoundError:
- self.skipTest("skill-seekers-package command not installed")
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_package_structure.py b/assets/repo/Skill_Seekers-development/tests/test_package_structure.py
deleted file mode 100644
index 0824401..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_package_structure.py
+++ /dev/null
@@ -1,223 +0,0 @@
-"""Test suite for Python package structure.
-
-Tests that the package structure is correct and imports work properly.
-This ensures modern Python packaging (src/ layout, pyproject.toml) is successful.
-"""
-
-import pytest
-import sys
-from pathlib import Path
-
-
-class TestCliPackage:
- """Test skill_seekers.cli package structure and imports."""
-
- def test_cli_package_exists(self):
- """Test that skill_seekers.cli package can be imported."""
- import skill_seekers.cli
- assert skill_seekers.cli is not None
-
- def test_cli_has_version(self):
- """Test that skill_seekers.cli package has __version__."""
- import skill_seekers.cli
- assert hasattr(skill_seekers.cli, '__version__')
- assert skill_seekers.cli.__version__ == '2.0.0'
-
- def test_cli_has_all(self):
- """Test that skill_seekers.cli package has __all__ export list."""
- import skill_seekers.cli
- assert hasattr(skill_seekers.cli, '__all__')
- assert isinstance(skill_seekers.cli.__all__, list)
- assert len(skill_seekers.cli.__all__) > 0
-
- def test_llms_txt_detector_import(self):
- """Test that LlmsTxtDetector can be imported from skill_seekers.cli."""
- from skill_seekers.cli import LlmsTxtDetector
- assert LlmsTxtDetector is not None
-
- def test_llms_txt_downloader_import(self):
- """Test that LlmsTxtDownloader can be imported from skill_seekers.cli."""
- from skill_seekers.cli import LlmsTxtDownloader
- assert LlmsTxtDownloader is not None
-
- def test_llms_txt_parser_import(self):
- """Test that LlmsTxtParser can be imported from skill_seekers.cli."""
- from skill_seekers.cli import LlmsTxtParser
- assert LlmsTxtParser is not None
-
- def test_open_folder_import(self):
- """Test that open_folder can be imported from skill_seekers.cli (if utils exists)."""
- try:
- from skill_seekers.cli import open_folder
- # If import succeeds, function should not be None
- assert open_folder is not None
- except ImportError:
- # If utils.py doesn't exist, that's okay for now
- pytest.skip("utils.py not found, skipping open_folder test")
-
- def test_cli_exports_match_all(self):
- """Test that exported items in __all__ can actually be imported."""
- import skill_seekers.cli as cli
- for item_name in cli.__all__:
- if item_name == 'open_folder' and cli.open_folder is None:
- # open_folder might be None if utils doesn't exist
- continue
- assert hasattr(cli, item_name), f"{item_name} not found in cli package"
-
-
-class TestMcpPackage:
- """Test skill_seekers.mcp package structure and imports."""
-
- def test_mcp_package_exists(self):
- """Test that skill_seekers.mcp package can be imported."""
- import skill_seekers.mcp
- assert skill_seekers.mcp is not None
-
- def test_mcp_has_version(self):
- """Test that skill_seekers.mcp package has __version__."""
- import skill_seekers.mcp
- assert hasattr(skill_seekers.mcp, '__version__')
- assert skill_seekers.mcp.__version__ == '2.0.0'
-
- def test_mcp_has_all(self):
- """Test that skill_seekers.mcp package has __all__ export list."""
- import skill_seekers.mcp
- assert hasattr(skill_seekers.mcp, '__all__')
- assert isinstance(skill_seekers.mcp.__all__, list)
-
- def test_mcp_tools_package_exists(self):
- """Test that skill_seekers.mcp.tools subpackage can be imported."""
- import skill_seekers.mcp.tools
- assert skill_seekers.mcp.tools is not None
-
- def test_mcp_tools_has_version(self):
- """Test that skill_seekers.mcp.tools has __version__."""
- import skill_seekers.mcp.tools
- assert hasattr(skill_seekers.mcp.tools, '__version__')
- assert skill_seekers.mcp.tools.__version__ == '2.0.0'
-
-
-class TestPackageStructure:
- """Test overall package structure integrity (src/ layout)."""
-
- def test_cli_init_file_exists(self):
- """Test that src/skill_seekers/cli/__init__.py exists."""
- init_file = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli' / '__init__.py'
- assert init_file.exists(), "src/skill_seekers/cli/__init__.py not found"
-
- def test_mcp_init_file_exists(self):
- """Test that src/skill_seekers/mcp/__init__.py exists."""
- init_file = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'mcp' / '__init__.py'
- assert init_file.exists(), "src/skill_seekers/mcp/__init__.py not found"
-
- def test_mcp_tools_init_file_exists(self):
- """Test that src/skill_seekers/mcp/tools/__init__.py exists."""
- init_file = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'mcp' / 'tools' / '__init__.py'
- assert init_file.exists(), "src/skill_seekers/mcp/tools/__init__.py not found"
-
- def test_cli_init_has_docstring(self):
- """Test that skill_seekers.cli/__init__.py has a module docstring."""
- import skill_seekers.cli
- assert skill_seekers.cli.__doc__ is not None
- assert len(skill_seekers.cli.__doc__) > 50 # Should have substantial documentation
-
- def test_mcp_init_has_docstring(self):
- """Test that skill_seekers.mcp/__init__.py has a module docstring."""
- import skill_seekers.mcp
- assert skill_seekers.mcp.__doc__ is not None
- assert len(skill_seekers.mcp.__doc__) > 50 # Should have substantial documentation
-
-
-class TestImportPatterns:
- """Test that various import patterns work correctly."""
-
- def test_direct_module_import(self):
- """Test importing modules directly."""
- from skill_seekers.cli import llms_txt_detector
- from skill_seekers.cli import llms_txt_downloader
- from skill_seekers.cli import llms_txt_parser
- assert llms_txt_detector is not None
- assert llms_txt_downloader is not None
- assert llms_txt_parser is not None
-
- def test_class_import_from_package(self):
- """Test importing classes from package."""
- from skill_seekers.cli import LlmsTxtDetector, LlmsTxtDownloader, LlmsTxtParser
- assert LlmsTxtDetector.__name__ == 'LlmsTxtDetector'
- assert LlmsTxtDownloader.__name__ == 'LlmsTxtDownloader'
- assert LlmsTxtParser.__name__ == 'LlmsTxtParser'
-
- def test_package_level_import(self):
- """Test importing entire packages."""
- import skill_seekers
- import skill_seekers.cli
- import skill_seekers.mcp
- import skill_seekers.mcp.tools
- assert 'skill_seekers' in sys.modules
- assert 'skill_seekers.cli' in sys.modules
- assert 'skill_seekers.mcp' in sys.modules
- assert 'skill_seekers.mcp.tools' in sys.modules
-
-
-class TestBackwardsCompatibility:
- """Test that existing code patterns still work."""
-
- def test_direct_file_import_still_works(self):
- """Test that direct file imports still work (backwards compatible)."""
- # This ensures we didn't break existing code
- from skill_seekers.cli.llms_txt_detector import LlmsTxtDetector
- from skill_seekers.cli.llms_txt_downloader import LlmsTxtDownloader
- from skill_seekers.cli.llms_txt_parser import LlmsTxtParser
- assert LlmsTxtDetector is not None
- assert LlmsTxtDownloader is not None
- assert LlmsTxtParser is not None
-
- def test_module_path_import_still_works(self):
- """Test that full module path imports still work."""
- import skill_seekers.cli.llms_txt_detector
- import skill_seekers.cli.llms_txt_downloader
- import skill_seekers.cli.llms_txt_parser
- assert skill_seekers.cli.llms_txt_detector is not None
- assert skill_seekers.cli.llms_txt_downloader is not None
- assert skill_seekers.cli.llms_txt_parser is not None
-
-
-class TestRootPackage:
- """Test root skill_seekers package."""
-
- def test_root_package_exists(self):
- """Test that skill_seekers root package can be imported."""
- import skill_seekers
- assert skill_seekers is not None
-
- def test_root_has_version(self):
- """Test that skill_seekers root package has __version__."""
- import skill_seekers
- assert hasattr(skill_seekers, '__version__')
- assert skill_seekers.__version__ == '2.0.0'
-
- def test_root_has_metadata(self):
- """Test that skill_seekers root package has metadata."""
- import skill_seekers
- assert hasattr(skill_seekers, '__author__')
- assert hasattr(skill_seekers, '__license__')
- assert skill_seekers.__license__ == 'MIT'
-
-
-class TestCLIEntryPoints:
- """Test that CLI entry points are properly configured."""
-
- def test_main_cli_module_exists(self):
- """Test that main.py module exists and can be imported."""
- from skill_seekers.cli import main
- assert main is not None
- assert hasattr(main, 'main')
- assert callable(main.main)
-
- def test_main_cli_has_parser(self):
- """Test that main.py has parser creation function."""
- from skill_seekers.cli.main import create_parser
- parser = create_parser()
- assert parser is not None
- # Test that main subcommands are configured
- assert parser.prog == 'skill-seekers'
diff --git a/assets/repo/Skill_Seekers-development/tests/test_parallel_scraping.py b/assets/repo/Skill_Seekers-development/tests/test_parallel_scraping.py
deleted file mode 100644
index 86fb181..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_parallel_scraping.py
+++ /dev/null
@@ -1,352 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for parallel scraping, unlimited mode, and rate limiting features (PR #144)
-"""
-
-import sys
-import os
-import unittest
-import tempfile
-import json
-import time
-from pathlib import Path
-from unittest.mock import Mock, patch, MagicMock
-from collections import deque
-
-from skill_seekers.cli.doc_scraper import DocToSkillConverter
-
-
-class TestParallelScrapingConfiguration(unittest.TestCase):
- """Test parallel scraping configuration and initialization"""
-
- def setUp(self):
- """Save original working directory"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Restore original working directory"""
- os.chdir(self.original_cwd)
-
- def test_single_worker_default(self):
- """Test default is single-worker mode"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': 10
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.workers, 1)
- self.assertFalse(hasattr(converter, 'lock'))
-
- def test_multiple_workers_creates_lock(self):
- """Test multiple workers creates thread lock"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': 10,
- 'workers': 4
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.workers, 4)
- self.assertTrue(hasattr(converter, 'lock'))
-
- def test_workers_from_config(self):
- """Test workers parameter is read from config"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'workers': 8
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.workers, 8)
-
-
-class TestUnlimitedMode(unittest.TestCase):
- """Test unlimited scraping mode"""
-
- def setUp(self):
- """Save original working directory"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Restore original working directory"""
- os.chdir(self.original_cwd)
-
- def test_unlimited_with_none(self):
- """Test max_pages: None enables unlimited mode"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': None
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertIsNone(converter.config.get('max_pages'))
-
- def test_unlimited_with_minus_one(self):
- """Test max_pages: -1 enables unlimited mode"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': -1
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.config.get('max_pages'), -1)
-
- def test_limited_mode_default(self):
- """Test default max_pages is limited"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'}
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- max_pages = converter.config.get('max_pages', 500)
- self.assertIsNotNone(max_pages)
- self.assertGreater(max_pages, 0)
-
-
-class TestRateLimiting(unittest.TestCase):
- """Test rate limiting configuration"""
-
- def setUp(self):
- """Save original working directory"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Restore original working directory"""
- os.chdir(self.original_cwd)
-
- def test_rate_limit_from_config(self):
- """Test rate_limit is read from config"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'rate_limit': 0.1
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.config.get('rate_limit'), 0.1)
-
- def test_rate_limit_default(self):
- """Test default rate_limit is 0.5"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'}
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.config.get('rate_limit', 0.5), 0.5)
-
- def test_zero_rate_limit_disables(self):
- """Test rate_limit: 0 disables rate limiting"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'rate_limit': 0
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.config.get('rate_limit'), 0)
-
-
-class TestThreadSafety(unittest.TestCase):
- """Test thread-safety fixes"""
-
- def setUp(self):
- """Save original working directory"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Restore original working directory"""
- os.chdir(self.original_cwd)
-
- def test_lock_protects_visited_urls(self):
- """Test visited_urls operations are protected by lock"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'workers': 4
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
-
- # Verify lock exists
- self.assertTrue(hasattr(converter, 'lock'))
-
- # Verify it's a threading.Lock
- import threading
- self.assertIsInstance(converter.lock, type(threading.Lock()))
-
- def test_single_worker_no_lock(self):
- """Test single worker doesn't create unnecessary lock"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'workers': 1
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertFalse(hasattr(converter, 'lock'))
-
-
-class TestScrapingModes(unittest.TestCase):
- """Test different scraping mode combinations"""
-
- def setUp(self):
- """Save original working directory"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Restore original working directory"""
- os.chdir(self.original_cwd)
-
- def test_single_threaded_limited(self):
- """Test traditional single-threaded limited mode"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': 10,
- 'workers': 1
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.workers, 1)
- self.assertEqual(converter.config.get('max_pages'), 10)
-
- def test_parallel_limited(self):
- """Test parallel scraping with page limit"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': 100,
- 'workers': 4
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.workers, 4)
- self.assertEqual(converter.config.get('max_pages'), 100)
- self.assertTrue(hasattr(converter, 'lock'))
-
- def test_parallel_unlimited(self):
- """Test parallel scraping with unlimited pages"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': None,
- 'workers': 8
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.workers, 8)
- self.assertIsNone(converter.config.get('max_pages'))
- self.assertTrue(hasattr(converter, 'lock'))
-
- def test_fast_scraping_mode(self):
- """Test fast scraping with low rate limit and workers"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'rate_limit': 0.1,
- 'workers': 8,
- 'max_pages': 1000
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertEqual(converter.workers, 8)
- self.assertEqual(converter.config.get('rate_limit'), 0.1)
-
-
-class TestDryRunWithNewFeatures(unittest.TestCase):
- """Test dry-run mode works with new features"""
-
- def setUp(self):
- """Save original working directory"""
- self.original_cwd = os.getcwd()
-
- def tearDown(self):
- """Restore original working directory"""
- os.chdir(self.original_cwd)
-
- def test_dry_run_with_parallel(self):
- """Test dry-run with parallel workers"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'workers': 4
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertTrue(converter.dry_run)
- self.assertEqual(converter.workers, 4)
-
- def test_dry_run_with_unlimited(self):
- """Test dry-run with unlimited mode"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'max_pages': None
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertTrue(converter.dry_run)
- self.assertIsNone(converter.config.get('max_pages'))
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_pdf_advanced_features.py b/assets/repo/Skill_Seekers-development/tests/test_pdf_advanced_features.py
deleted file mode 100644
index 892d041..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_pdf_advanced_features.py
+++ /dev/null
@@ -1,524 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for PDF Advanced Features (Priority 2 & 3)
-
-Tests cover:
-- OCR support for scanned PDFs
-- Password-protected PDFs
-- Table extraction
-- Parallel processing
-- Caching
-"""
-
-import unittest
-import sys
-import tempfile
-import shutil
-import io
-from pathlib import Path
-from unittest.mock import Mock, patch, MagicMock
-
-# Add parent directory to path for imports
-sys.path.insert(0, str(Path(__file__).parent.parent / "cli"))
-
-try:
- import fitz # PyMuPDF
- PYMUPDF_AVAILABLE = True
-except ImportError:
- PYMUPDF_AVAILABLE = False
-
-try:
- from PIL import Image
- import pytesseract
- TESSERACT_AVAILABLE = True
-except ImportError:
- TESSERACT_AVAILABLE = False
-
-
-class TestOCRSupport(unittest.TestCase):
- """Test OCR support for scanned PDFs (Priority 2)"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- if hasattr(self, 'temp_dir'):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_ocr_initialization(self):
- """Test OCR flag initialization"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.use_ocr = True
- self.assertTrue(extractor.use_ocr)
-
- def test_extract_text_with_ocr_disabled(self):
- """Test that OCR can be disabled"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.use_ocr = False
- extractor.verbose = False
-
- # Create mock page with normal text
- mock_page = Mock()
- mock_page.get_text.return_value = "This is regular text"
-
- text = extractor.extract_text_with_ocr(mock_page)
-
- self.assertEqual(text, "This is regular text")
- mock_page.get_text.assert_called_once_with("text")
-
- def test_extract_text_with_ocr_sufficient_text(self):
- """Test OCR not triggered when sufficient text exists"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.use_ocr = True
- extractor.verbose = False
-
- # Create mock page with enough text
- mock_page = Mock()
- mock_page.get_text.return_value = "This is a long paragraph with more than 50 characters"
-
- text = extractor.extract_text_with_ocr(mock_page)
-
- self.assertEqual(len(text), 53) # Length after .strip()
- # OCR should not be triggered
- mock_page.get_pixmap.assert_not_called()
-
- @patch('pdf_extractor_poc.TESSERACT_AVAILABLE', False)
- def test_ocr_unavailable_warning(self):
- """Test warning when OCR requested but pytesseract not available"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.use_ocr = True
- extractor.verbose = True
-
- mock_page = Mock()
- mock_page.get_text.return_value = "Short" # Less than 50 chars
-
- # Capture output
- with patch('sys.stdout', new=io.StringIO()) as fake_out:
- text = extractor.extract_text_with_ocr(mock_page)
- output = fake_out.getvalue()
-
- self.assertIn("OCR requested but pytesseract not installed", output)
- self.assertEqual(text, "Short")
-
- @unittest.skipUnless(TESSERACT_AVAILABLE, "pytesseract not installed")
- def test_ocr_extraction_triggered(self):
- """Test OCR extraction when text is minimal"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.use_ocr = True
- extractor.verbose = False
-
- # Create mock page with minimal text
- mock_page = Mock()
- mock_page.get_text.return_value = "X" # Less than 50 chars
-
- # Mock pixmap and PIL Image
- mock_pix = Mock()
- mock_pix.width = 100
- mock_pix.height = 100
- mock_pix.samples = b'\x00' * (100 * 100 * 3)
- mock_page.get_pixmap.return_value = mock_pix
-
- with patch('pytesseract.image_to_string', return_value="OCR extracted text here"):
- text = extractor.extract_text_with_ocr(mock_page)
-
- # Should use OCR text since it's longer
- self.assertEqual(text, "OCR extracted text here")
- mock_page.get_pixmap.assert_called_once()
-
-
-class TestPasswordProtection(unittest.TestCase):
- """Test password-protected PDF support (Priority 2)"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- if hasattr(self, 'temp_dir'):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_password_initialization(self):
- """Test password parameter initialization"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.password = "test_password"
- self.assertEqual(extractor.password, "test_password")
-
- def test_encrypted_pdf_detection(self):
- """Test detection of encrypted PDF"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.pdf_path = "test.pdf"
- extractor.password = "mypassword"
- extractor.verbose = False
-
- # Mock encrypted document (use MagicMock for __len__)
- mock_doc = MagicMock()
- mock_doc.is_encrypted = True
- mock_doc.authenticate.return_value = True
- mock_doc.metadata = {}
- mock_doc.__len__.return_value = 10
-
- with patch('fitz.open', return_value=mock_doc):
- # This would be called in extract_all()
- doc = fitz.open(extractor.pdf_path)
-
- self.assertTrue(doc.is_encrypted)
- result = doc.authenticate(extractor.password)
- self.assertTrue(result)
-
- def test_wrong_password_handling(self):
- """Test handling of wrong password"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.pdf_path = "test.pdf"
- extractor.password = "wrong_password"
-
- mock_doc = Mock()
- mock_doc.is_encrypted = True
- mock_doc.authenticate.return_value = False
-
- with patch('fitz.open', return_value=mock_doc):
- doc = fitz.open(extractor.pdf_path)
- result = doc.authenticate(extractor.password)
-
- self.assertFalse(result)
-
- def test_missing_password_for_encrypted_pdf(self):
- """Test error when password is missing for encrypted PDF"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.pdf_path = "test.pdf"
- extractor.password = None
-
- mock_doc = Mock()
- mock_doc.is_encrypted = True
-
- with patch('fitz.open', return_value=mock_doc):
- doc = fitz.open(extractor.pdf_path)
-
- self.assertTrue(doc.is_encrypted)
- self.assertIsNone(extractor.password)
-
-
-class TestTableExtraction(unittest.TestCase):
- """Test table extraction (Priority 2)"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- if hasattr(self, 'temp_dir'):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_table_extraction_initialization(self):
- """Test table extraction flag initialization"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.extract_tables = True
- self.assertTrue(extractor.extract_tables)
-
- def test_table_extraction_disabled(self):
- """Test no tables extracted when disabled"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.extract_tables = False
- extractor.verbose = False
-
- mock_page = Mock()
- tables = extractor.extract_tables_from_page(mock_page)
-
- self.assertEqual(tables, [])
- # find_tables should not be called
- mock_page.find_tables.assert_not_called()
-
- def test_table_extraction_basic(self):
- """Test basic table extraction"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.extract_tables = True
- extractor.verbose = False
-
- # Create mock table
- mock_table = Mock()
- mock_table.extract.return_value = [
- ["Header 1", "Header 2", "Header 3"],
- ["Data 1", "Data 2", "Data 3"]
- ]
- mock_table.bbox = (0, 0, 100, 100)
-
- # Create mock tables result
- mock_tables = Mock()
- mock_tables.tables = [mock_table]
-
- mock_page = Mock()
- mock_page.find_tables.return_value = mock_tables
-
- tables = extractor.extract_tables_from_page(mock_page)
-
- self.assertEqual(len(tables), 1)
- self.assertEqual(tables[0]['row_count'], 2)
- self.assertEqual(tables[0]['col_count'], 3)
- self.assertEqual(tables[0]['table_index'], 0)
-
- def test_multiple_tables_extraction(self):
- """Test extraction of multiple tables from one page"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.extract_tables = True
- extractor.verbose = False
-
- # Create two mock tables
- mock_table1 = Mock()
- mock_table1.extract.return_value = [["A", "B"], ["1", "2"]]
- mock_table1.bbox = (0, 0, 50, 50)
-
- mock_table2 = Mock()
- mock_table2.extract.return_value = [["X", "Y", "Z"], ["10", "20", "30"]]
- mock_table2.bbox = (0, 60, 50, 110)
-
- mock_tables = Mock()
- mock_tables.tables = [mock_table1, mock_table2]
-
- mock_page = Mock()
- mock_page.find_tables.return_value = mock_tables
-
- tables = extractor.extract_tables_from_page(mock_page)
-
- self.assertEqual(len(tables), 2)
- self.assertEqual(tables[0]['table_index'], 0)
- self.assertEqual(tables[1]['table_index'], 1)
-
- def test_table_extraction_error_handling(self):
- """Test error handling during table extraction"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.extract_tables = True
- extractor.verbose = False
-
- mock_page = Mock()
- mock_page.find_tables.side_effect = Exception("Table extraction failed")
-
- # Should not raise, should return empty list
- tables = extractor.extract_tables_from_page(mock_page)
-
- self.assertEqual(tables, [])
-
-
-class TestCaching(unittest.TestCase):
- """Test caching of expensive operations (Priority 3)"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- if hasattr(self, 'temp_dir'):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_cache_initialization(self):
- """Test cache is initialized"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor._cache = {}
- extractor.use_cache = True
-
- self.assertIsInstance(extractor._cache, dict)
- self.assertTrue(extractor.use_cache)
-
- def test_cache_set_and_get(self):
- """Test setting and getting cached values"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor._cache = {}
- extractor.use_cache = True
-
- # Set cache
- test_data = {"page": 1, "text": "cached content"}
- extractor.set_cached("page_1", test_data)
-
- # Get cache
- cached = extractor.get_cached("page_1")
-
- self.assertEqual(cached, test_data)
-
- def test_cache_miss(self):
- """Test cache miss returns None"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor._cache = {}
- extractor.use_cache = True
-
- cached = extractor.get_cached("nonexistent_key")
-
- self.assertIsNone(cached)
-
- def test_cache_disabled(self):
- """Test caching can be disabled"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor._cache = {}
- extractor.use_cache = False
-
- # Try to set cache
- extractor.set_cached("page_1", {"data": "test"})
-
- # Cache should be empty
- self.assertEqual(len(extractor._cache), 0)
-
- # Try to get cache
- cached = extractor.get_cached("page_1")
- self.assertIsNone(cached)
-
- def test_cache_overwrite(self):
- """Test cache can be overwritten"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor._cache = {}
- extractor.use_cache = True
-
- # Set initial value
- extractor.set_cached("page_1", {"version": 1})
-
- # Overwrite
- extractor.set_cached("page_1", {"version": 2})
-
- # Get cached value
- cached = extractor.get_cached("page_1")
-
- self.assertEqual(cached["version"], 2)
-
-
-class TestParallelProcessing(unittest.TestCase):
- """Test parallel page processing (Priority 3)"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- if hasattr(self, 'temp_dir'):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_parallel_initialization(self):
- """Test parallel processing flag initialization"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.parallel = True
- extractor.max_workers = 4
-
- self.assertTrue(extractor.parallel)
- self.assertEqual(extractor.max_workers, 4)
-
- def test_parallel_disabled_by_default(self):
- """Test parallel processing is disabled by default"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.parallel = False
-
- self.assertFalse(extractor.parallel)
-
- def test_worker_count_auto_detect(self):
- """Test worker count auto-detection"""
- import os
- cpu_count = os.cpu_count()
-
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.max_workers = cpu_count
-
- self.assertIsNotNone(extractor.max_workers)
- self.assertGreater(extractor.max_workers, 0)
-
- def test_custom_worker_count(self):
- """Test custom worker count"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.max_workers = 8
-
- self.assertEqual(extractor.max_workers, 8)
-
-
-class TestIntegration(unittest.TestCase):
- """Integration tests for advanced features"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- if hasattr(self, 'temp_dir'):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_full_initialization_with_all_features(self):
- """Test initialization with all advanced features enabled"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
-
- # Set all advanced features
- extractor.use_ocr = True
- extractor.password = "test_password"
- extractor.extract_tables = True
- extractor.parallel = True
- extractor.max_workers = 4
- extractor.use_cache = True
- extractor._cache = {}
-
- # Verify all features are set
- self.assertTrue(extractor.use_ocr)
- self.assertEqual(extractor.password, "test_password")
- self.assertTrue(extractor.extract_tables)
- self.assertTrue(extractor.parallel)
- self.assertEqual(extractor.max_workers, 4)
- self.assertTrue(extractor.use_cache)
-
- def test_feature_combinations(self):
- """Test various feature combinations"""
- combinations = [
- {"use_ocr": True, "extract_tables": True},
- {"password": "test", "parallel": True},
- {"use_cache": True, "extract_tables": True, "parallel": True},
- {"use_ocr": True, "password": "test", "extract_tables": True, "parallel": True}
- ]
-
- for combo in combinations:
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- for key, value in combo.items():
- setattr(extractor, key, value)
-
- # Verify all attributes are set correctly
- for key, value in combo.items():
- self.assertEqual(getattr(extractor, key), value)
-
- def test_page_data_includes_tables(self):
- """Test that page data includes table count"""
- # This tests that the page_data structure includes tables
- expected_keys = [
- 'page_number', 'text', 'markdown', 'headings',
- 'code_samples', 'images_count', 'extracted_images',
- 'tables', 'char_count', 'code_blocks_count', 'tables_count'
- ]
-
- # Just verify the structure is correct
- # Actual extraction is tested in other test classes
- page_data = {
- 'page_number': 1,
- 'text': 'test',
- 'markdown': 'test',
- 'headings': [],
- 'code_samples': [],
- 'images_count': 0,
- 'extracted_images': [],
- 'tables': [],
- 'char_count': 4,
- 'code_blocks_count': 0,
- 'tables_count': 0
- }
-
- for key in expected_keys:
- self.assertIn(key, page_data)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_pdf_extractor.py b/assets/repo/Skill_Seekers-development/tests/test_pdf_extractor.py
deleted file mode 100644
index 5e8d243..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_pdf_extractor.py
+++ /dev/null
@@ -1,404 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for PDF Extractor (cli/pdf_extractor_poc.py)
-
-Tests cover:
-- Language detection with confidence scoring
-- Code block detection (font, indent, pattern)
-- Syntax validation
-- Quality scoring
-- Chapter detection
-- Page chunking
-- Code block merging
-"""
-
-import unittest
-import sys
-from pathlib import Path
-
-# Add parent directory to path for imports
-sys.path.insert(0, str(Path(__file__).parent.parent / "cli"))
-
-try:
- import fitz # PyMuPDF
- PYMUPDF_AVAILABLE = True
-except ImportError:
- PYMUPDF_AVAILABLE = False
-
-
-class TestLanguageDetection(unittest.TestCase):
- """Test language detection with confidence scoring"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
-
- def test_detect_python_with_confidence(self):
- """Test Python detection returns language and confidence"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "def hello():\n print('world')\n return True"
-
- language, confidence = extractor.detect_language_from_code(code)
-
- self.assertEqual(language, "python")
- self.assertGreater(confidence, 0.4) # Should have reasonable confidence
- self.assertLessEqual(confidence, 1.0)
-
- def test_detect_javascript_with_confidence(self):
- """Test JavaScript detection"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "const handleClick = () => {\n console.log('clicked');\n};"
-
- language, confidence = extractor.detect_language_from_code(code)
-
- self.assertEqual(language, "javascript")
- self.assertGreater(confidence, 0.5)
-
- def test_detect_cpp_with_confidence(self):
- """Test C++ detection"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "#include \nint main() {\n std::cout << \"Hello\";\n}"
-
- language, confidence = extractor.detect_language_from_code(code)
-
- self.assertEqual(language, "cpp")
- self.assertGreater(confidence, 0.5)
-
- def test_detect_unknown_low_confidence(self):
- """Test unknown language returns low confidence"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "this is not code at all just plain text"
-
- language, confidence = extractor.detect_language_from_code(code)
-
- self.assertEqual(language, "unknown")
- self.assertLess(confidence, 0.3) # Should be low confidence
-
- def test_confidence_range(self):
- """Test confidence is always between 0 and 1"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- test_codes = [
- "def foo(): pass",
- "const x = 10;",
- "#include ",
- "random text here",
- ""
- ]
-
- for code in test_codes:
- _, confidence = extractor.detect_language_from_code(code)
- self.assertGreaterEqual(confidence, 0.0)
- self.assertLessEqual(confidence, 1.0)
-
-
-class TestSyntaxValidation(unittest.TestCase):
- """Test syntax validation for different languages"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
-
- def test_validate_python_valid(self):
- """Test valid Python syntax"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "def hello():\n print('world')\n return True"
-
- is_valid, issues = extractor.validate_code_syntax(code, "python")
-
- self.assertTrue(is_valid)
- self.assertEqual(len(issues), 0)
-
- def test_validate_python_invalid_indentation(self):
- """Test invalid Python indentation"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "def hello():\n print('world')\n\tprint('mixed')" # Mixed tabs and spaces
-
- is_valid, issues = extractor.validate_code_syntax(code, "python")
-
- self.assertFalse(is_valid)
- self.assertGreater(len(issues), 0)
-
- def test_validate_python_unbalanced_brackets(self):
- """Test unbalanced brackets"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "x = [[[1, 2, 3" # Severely unbalanced brackets
-
- is_valid, issues = extractor.validate_code_syntax(code, "python")
-
- self.assertFalse(is_valid)
- self.assertGreater(len(issues), 0)
-
- def test_validate_javascript_valid(self):
- """Test valid JavaScript syntax"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "const x = () => { return 42; };"
-
- is_valid, issues = extractor.validate_code_syntax(code, "javascript")
-
- self.assertTrue(is_valid)
- self.assertEqual(len(issues), 0)
-
- def test_validate_natural_language_fails(self):
- """Test natural language fails validation"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "This is just a regular sentence with the and for and with and that and have and from words."
-
- is_valid, issues = extractor.validate_code_syntax(code, "python")
-
- self.assertFalse(is_valid)
- self.assertIn('May be natural language', ' '.join(issues))
-
-
-class TestQualityScoring(unittest.TestCase):
- """Test code quality scoring (0-10 scale)"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
-
- def test_quality_score_range(self):
- """Test quality score is between 0 and 10"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "def hello():\n print('world')"
-
- quality = extractor.score_code_quality(code, "python", 0.8)
-
- self.assertGreaterEqual(quality, 0.0)
- self.assertLessEqual(quality, 10.0)
-
- def test_high_quality_code(self):
- """Test high-quality code gets good score"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = """def calculate_sum(numbers):
- '''Calculate sum of numbers'''
- total = 0
- for num in numbers:
- total += num
- return total"""
-
- quality = extractor.score_code_quality(code, "python", 0.9)
-
- self.assertGreater(quality, 6.0) # Should be good quality
-
- def test_low_quality_code(self):
- """Test low-quality code gets low score"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- code = "x" # Too short, no structure
-
- quality = extractor.score_code_quality(code, "unknown", 0.1)
-
- self.assertLess(quality, 6.0) # Should be low quality
-
- def test_quality_factors(self):
- """Test that quality considers multiple factors"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
-
- # Good: proper structure, indentation, confidence
- good_code = "def foo():\n return bar()"
- good_quality = extractor.score_code_quality(good_code, "python", 0.9)
-
- # Bad: no structure, low confidence
- bad_code = "some text"
- bad_quality = extractor.score_code_quality(bad_code, "unknown", 0.1)
-
- self.assertGreater(good_quality, bad_quality)
-
-
-class TestChapterDetection(unittest.TestCase):
- """Test chapter/section detection"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
-
- def test_detect_chapter_with_number(self):
- """Test chapter detection with number"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- page_data = {
- 'text': 'Chapter 1: Introduction to Python\nThis is the first chapter.',
- 'headings': []
- }
-
- is_chapter, title = extractor.detect_chapter_start(page_data)
-
- self.assertTrue(is_chapter)
- self.assertIsNotNone(title)
-
- def test_detect_chapter_uppercase(self):
- """Test chapter detection with uppercase"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- page_data = {
- 'text': 'Chapter 1\nThis is the introduction', # Pattern requires Chapter + digit
- 'headings': []
- }
-
- is_chapter, title = extractor.detect_chapter_start(page_data)
-
- self.assertTrue(is_chapter)
-
- def test_detect_section_heading(self):
- """Test section heading detection"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- page_data = {
- 'text': '2. Getting Started\nThis is a section.',
- 'headings': []
- }
-
- is_chapter, title = extractor.detect_chapter_start(page_data)
-
- self.assertTrue(is_chapter)
-
- def test_not_chapter(self):
- """Test normal text is not detected as chapter"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- page_data = {
- 'text': 'This is just normal paragraph text without any chapter markers.',
- 'headings': []
- }
-
- is_chapter, title = extractor.detect_chapter_start(page_data)
-
- self.assertFalse(is_chapter)
-
-
-class TestCodeBlockMerging(unittest.TestCase):
- """Test code block merging across pages"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
-
- def test_merge_continued_blocks(self):
- """Test merging code blocks split across pages"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.verbose = False # Initialize verbose attribute
-
- pages = [
- {
- 'page_number': 1,
- 'code_samples': [
- {'code': 'def hello():', 'language': 'python', 'detection_method': 'pattern'}
- ],
- 'code_blocks_count': 1
- },
- {
- 'page_number': 2,
- 'code_samples': [
- {'code': ' print("world")', 'language': 'python', 'detection_method': 'pattern'}
- ],
- 'code_blocks_count': 1
- }
- ]
-
- merged = extractor.merge_continued_code_blocks(pages)
-
- # Should have merged the two blocks
- self.assertIn('def hello():', merged[0]['code_samples'][0]['code'])
- self.assertIn('print("world")', merged[0]['code_samples'][0]['code'])
-
- def test_no_merge_different_languages(self):
- """Test blocks with different languages are not merged"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
-
- pages = [
- {
- 'page_number': 1,
- 'code_samples': [
- {'code': 'def foo():', 'language': 'python', 'detection_method': 'pattern'}
- ],
- 'code_blocks_count': 1
- },
- {
- 'page_number': 2,
- 'code_samples': [
- {'code': 'const x = 10;', 'language': 'javascript', 'detection_method': 'pattern'}
- ],
- 'code_blocks_count': 1
- }
- ]
-
- merged = extractor.merge_continued_code_blocks(pages)
-
- # Should NOT merge different languages
- self.assertEqual(len(merged[0]['code_samples']), 1)
- self.assertEqual(len(merged[1]['code_samples']), 1)
-
-
-class TestCodeDetectionMethods(unittest.TestCase):
- """Test different code detection methods"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
-
- def test_pattern_based_detection(self):
- """Test pattern-based code detection"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
-
- # Should detect function definitions
- text = "Here is an example:\ndef calculate(x, y):\n return x + y"
-
- # Pattern-based detection should find this
- # (implementation details depend on pdf_extractor_poc.py)
- self.assertIn("def ", text)
- self.assertIn("return", text)
-
- def test_indent_based_detection(self):
- """Test indent-based code detection"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
-
- # Code with consistent indentation
- indented_text = """ def foo():
- return bar()"""
-
- # Should detect as code due to indentation
- self.assertTrue(indented_text.startswith(" " * 4))
-
-
-class TestQualityFiltering(unittest.TestCase):
- """Test quality-based filtering"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from pdf_extractor_poc import PDFExtractor
- self.PDFExtractor = PDFExtractor
-
- def test_filter_by_min_quality(self):
- """Test filtering code blocks by minimum quality"""
- extractor = self.PDFExtractor.__new__(self.PDFExtractor)
- extractor.min_quality = 5.0
-
- # High quality block
- high_quality = {
- 'code': 'def calculate():\n return 42',
- 'language': 'python',
- 'quality': 8.0
- }
-
- # Low quality block
- low_quality = {
- 'code': 'x',
- 'language': 'unknown',
- 'quality': 2.0
- }
-
- # Only high quality should pass
- self.assertGreaterEqual(high_quality['quality'], extractor.min_quality)
- self.assertLess(low_quality['quality'], extractor.min_quality)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_pdf_scraper.py b/assets/repo/Skill_Seekers-development/tests/test_pdf_scraper.py
deleted file mode 100644
index fed6a4f..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_pdf_scraper.py
+++ /dev/null
@@ -1,602 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for PDF Scraper (cli/pdf_scraper.py)
-
-Tests cover:
-- Config-based PDF extraction
-- Direct PDF path conversion
-- JSON-based workflow
-- Skill structure generation
-- Categorization
-- Error handling
-"""
-
-import unittest
-import sys
-import json
-import tempfile
-import shutil
-from pathlib import Path
-from unittest.mock import Mock, patch, MagicMock
-
-try:
- import fitz # PyMuPDF
- PYMUPDF_AVAILABLE = True
-except ImportError:
- PYMUPDF_AVAILABLE = False
-
-
-class TestPDFToSkillConverter(unittest.TestCase):
- """Test PDFToSkillConverter initialization and basic functionality"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from skill_seekers.cli.pdf_scraper import PDFToSkillConverter
- self.PDFToSkillConverter = PDFToSkillConverter
-
- # Create temporary directory for test output
- self.temp_dir = tempfile.mkdtemp()
- self.output_dir = Path(self.temp_dir)
-
- def tearDown(self):
- # Clean up temporary directory
- if hasattr(self, 'temp_dir'):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_init_with_name_and_pdf_path(self):
- """Test initialization with name and PDF path"""
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- self.assertEqual(converter.name, "test_skill")
- self.assertEqual(converter.pdf_path, "test.pdf")
-
- def test_init_with_config(self):
- """Test initialization with config file"""
- # Create test config
- config = {
- "name": "config_skill",
- "description": "Test skill",
- "pdf_path": "docs/test.pdf",
- "extract_options": {
- "chunk_size": 10,
- "min_quality": 5.0
- }
- }
-
- converter = self.PDFToSkillConverter(config)
-
- self.assertEqual(converter.name, "config_skill")
- self.assertEqual(converter.config.get("description"), "Test skill")
-
- def test_init_requires_name_or_config(self):
- """Test that initialization requires config dict with 'name' field"""
- with self.assertRaises((ValueError, TypeError, KeyError)):
- self.PDFToSkillConverter({})
-
-
-class TestCategorization(unittest.TestCase):
- """Test content categorization functionality"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from skill_seekers.cli.pdf_scraper import PDFToSkillConverter
- self.PDFToSkillConverter = PDFToSkillConverter
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_categorize_by_keywords(self):
- """Test categorization using keyword matching"""
- config = {
- "name": "test",
- "pdf_path": "test.pdf",
- "categories": {
- "getting_started": ["introduction", "getting started"],
- "api": ["api", "reference", "function"]
- }
- }
-
- converter = self.PDFToSkillConverter(config)
-
- # Mock extracted data with different content
- converter.extracted_data = {
- "pages": [
- {
- "page_number": 1,
- "text": "Introduction to the API",
- "chapter": "Chapter 1: Getting Started"
- },
- {
- "page_number": 2,
- "text": "API reference for functions",
- "chapter": None
- }
- ]
- }
-
- categories = converter.categorize_content()
-
- # Should have both categories
- self.assertIn("getting_started", categories)
- self.assertIn("api", categories)
-
- def test_categorize_by_chapters(self):
- """Test categorization using chapter information"""
- config = {
- "name": "test",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- # Mock data with chapters
- converter.extracted_data = {
- "pages": [
- {
- "page_number": 1,
- "text": "Content here",
- "chapter": "Chapter 1: Introduction"
- },
- {
- "page_number": 2,
- "text": "More content",
- "chapter": "Chapter 1: Introduction"
- },
- {
- "page_number": 3,
- "text": "New chapter",
- "chapter": "Chapter 2: Advanced Topics"
- }
- ]
- }
-
- categories = converter.categorize_content()
-
- # Should create categories based on chapters
- self.assertIsInstance(categories, dict)
- self.assertGreater(len(categories), 0)
-
- def test_categorize_handles_no_chapters(self):
- """Test categorization when no chapters are detected"""
- config = {
- "name": "test",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- # Mock data without chapters
- converter.extracted_data = {
- "pages": [
- {
- "page_number": 1,
- "text": "Some content",
- "chapter": None
- }
- ]
- }
-
- categories = converter.categorize_content()
-
- # Should still create categories (fallback to "other")
- self.assertIsInstance(categories, dict)
-
-
-class TestSkillBuilding(unittest.TestCase):
- """Test skill structure generation"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from skill_seekers.cli.pdf_scraper import PDFToSkillConverter
- self.PDFToSkillConverter = PDFToSkillConverter
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_build_skill_creates_structure(self):
- """Test that build_skill creates required directory structure"""
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- # Override skill_dir to use temp directory
- converter.skill_dir = str(Path(self.temp_dir) / "test_skill")
-
- # Mock extracted data
- converter.extracted_data = {
- "pages": [
- {
- "page_number": 1,
- "text": "Test content",
- "code_blocks": [],
- "images": []
- }
- ],
- "total_pages": 1
- }
-
- # Mock categorization
- converter.categories = {
- "getting_started": [converter.extracted_data["pages"][0]]
- }
-
- converter.build_skill()
-
- # Check directory structure
- skill_dir = Path(self.temp_dir) / "test_skill"
- self.assertTrue(skill_dir.exists())
- self.assertTrue((skill_dir / "references").exists())
- self.assertTrue((skill_dir / "scripts").exists())
- self.assertTrue((skill_dir / "assets").exists())
-
- def test_build_skill_creates_skill_md(self):
- """Test that SKILL.md is created"""
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf",
- "description": "Test description"
- }
- converter = self.PDFToSkillConverter(config)
-
- # Override skill_dir to use temp directory
- converter.skill_dir = str(Path(self.temp_dir) / "test_skill")
-
- converter.extracted_data = {
- "pages": [{"page_number": 1, "text": "Test", "code_blocks": [], "images": []}],
- "total_pages": 1
- }
- converter.categories = {"test": [converter.extracted_data["pages"][0]]}
-
- converter.build_skill()
-
- skill_md = Path(self.temp_dir) / "test_skill" / "SKILL.md"
- self.assertTrue(skill_md.exists())
-
- # Check content
- content = skill_md.read_text()
- self.assertIn("test_skill", content)
- self.assertIn("Test description", content)
-
- def test_build_skill_creates_reference_files(self):
- """Test that reference files are created for categories"""
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- # Override skill_dir to use temp directory
- converter.skill_dir = str(Path(self.temp_dir) / "test_skill")
-
- converter.extracted_data = {
- "pages": [
- {"page_number": 1, "text": "Getting started", "code_blocks": [], "images": []},
- {"page_number": 2, "text": "API reference", "code_blocks": [], "images": []}
- ],
- "total_pages": 2
- }
-
- converter.categories = {
- "getting_started": [converter.extracted_data["pages"][0]],
- "api": [converter.extracted_data["pages"][1]]
- }
-
- converter.build_skill()
-
- # Check reference files exist
- refs_dir = Path(self.temp_dir) / "test_skill" / "references"
- self.assertTrue((refs_dir / "getting_started.md").exists())
- self.assertTrue((refs_dir / "api.md").exists())
- self.assertTrue((refs_dir / "index.md").exists())
-
-
-class TestCodeBlockHandling(unittest.TestCase):
- """Test code block extraction and inclusion in references"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from skill_seekers.cli.pdf_scraper import PDFToSkillConverter
- self.PDFToSkillConverter = PDFToSkillConverter
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_code_blocks_included_in_references(self):
- """Test that code blocks are included in reference files"""
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- # Override skill_dir to use temp directory
- converter.skill_dir = str(Path(self.temp_dir) / "test_skill")
-
- # Mock data with code blocks
- converter.extracted_data = {
- "pages": [
- {
- "page_number": 1,
- "text": "Example code",
- "code_blocks": [
- {
- "code": "def hello():\n print('world')",
- "language": "python",
- "quality": 8.0
- }
- ],
- "images": []
- }
- ],
- "total_pages": 1
- }
-
- converter.categories = {
- "examples": [converter.extracted_data["pages"][0]]
- }
-
- converter.build_skill()
-
- # Check code block in reference file
- ref_file = Path(self.temp_dir) / "test_skill" / "references" / "examples.md"
- content = ref_file.read_text()
-
- self.assertIn("```python", content)
- self.assertIn("def hello()", content)
- self.assertIn("print('world')", content)
-
- def test_high_quality_code_preferred(self):
- """Test that high-quality code blocks are prioritized"""
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- # Override skill_dir to use temp directory
- converter.skill_dir = str(Path(self.temp_dir) / "test_skill")
-
- # Mock data with varying quality
- converter.extracted_data = {
- "pages": [
- {
- "page_number": 1,
- "text": "Code examples",
- "code_blocks": [
- {"code": "x = 1", "language": "python", "quality": 2.0},
- {"code": "def process():\n return result", "language": "python", "quality": 9.0}
- ],
- "images": []
- }
- ],
- "total_pages": 1
- }
-
- converter.categories = {"examples": [converter.extracted_data["pages"][0]]}
- converter.build_skill()
-
- ref_file = Path(self.temp_dir) / "test_skill" / "references" / "examples.md"
- content = ref_file.read_text()
-
- # High quality code should be included
- self.assertIn("def process()", content)
-
-
-class TestImageHandling(unittest.TestCase):
- """Test image extraction and handling"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from skill_seekers.cli.pdf_scraper import PDFToSkillConverter
- self.PDFToSkillConverter = PDFToSkillConverter
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_images_saved_to_assets(self):
- """Test that images are saved to assets directory"""
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- # Override skill_dir to use temp directory
- converter.skill_dir = str(Path(self.temp_dir) / "test_skill")
-
- # Mock image data (1x1 white PNG)
- mock_image_bytes = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82'
-
- converter.extracted_data = {
- "pages": [
- {
- "page_number": 1,
- "text": "See diagram",
- "code_blocks": [],
- "images": [
- {
- "page": 1,
- "index": 0,
- "width": 100,
- "height": 100,
- "data": mock_image_bytes
- }
- ]
- }
- ],
- "total_pages": 1
- }
-
- converter.categories = {"diagrams": [converter.extracted_data["pages"][0]]}
- converter.build_skill()
-
- # Check assets directory has image
- assets_dir = Path(self.temp_dir) / "test_skill" / "assets"
- image_files = list(assets_dir.glob("*.png"))
- self.assertGreater(len(image_files), 0)
-
- def test_image_references_in_markdown(self):
- """Test that images are referenced in markdown files"""
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- # Override skill_dir to use temp directory
- converter.skill_dir = str(Path(self.temp_dir) / "test_skill")
-
- mock_image_bytes = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82'
-
- converter.extracted_data = {
- "pages": [
- {
- "page_number": 1,
- "text": "Architecture diagram",
- "code_blocks": [],
- "images": [
- {
- "page": 1,
- "index": 0,
- "width": 200,
- "height": 150,
- "data": mock_image_bytes
- }
- ]
- }
- ],
- "total_pages": 1
- }
-
- converter.categories = {"architecture": [converter.extracted_data["pages"][0]]}
- converter.build_skill()
-
- # Check markdown has image reference
- ref_file = Path(self.temp_dir) / "test_skill" / "references" / "architecture.md"
- content = ref_file.read_text()
-
- self.assertIn("![", content) # Markdown image syntax
- self.assertIn("../assets/", content) # Relative path to assets
-
-
-class TestErrorHandling(unittest.TestCase):
- """Test error handling for invalid inputs"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from skill_seekers.cli.pdf_scraper import PDFToSkillConverter
- self.PDFToSkillConverter = PDFToSkillConverter
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_missing_pdf_file(self):
- """Test error when PDF file doesn't exist"""
- config = {
- "name": "test",
- "pdf_path": "nonexistent.pdf"
- }
- converter = self.PDFToSkillConverter(config)
-
- with self.assertRaises((FileNotFoundError, RuntimeError)):
- converter.extract_pdf()
-
- def test_invalid_config_file(self):
- """Test error when config dict is invalid"""
- invalid_config = "invalid string not a dict"
-
- with self.assertRaises((ValueError, TypeError, AttributeError)):
- self.PDFToSkillConverter(invalid_config)
-
- def test_missing_required_config_fields(self):
- """Test error when config is missing required fields"""
- config = {"description": "Missing name and pdf_path"}
-
- with self.assertRaises((ValueError, KeyError)):
- converter = self.PDFToSkillConverter(config)
- converter.extract_pdf()
-
-
-class TestJSONWorkflow(unittest.TestCase):
- """Test building skills from extracted JSON"""
-
- def setUp(self):
- if not PYMUPDF_AVAILABLE:
- self.skipTest("PyMuPDF not installed")
- from skill_seekers.cli.pdf_scraper import PDFToSkillConverter
- self.PDFToSkillConverter = PDFToSkillConverter
- self.temp_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
- def test_load_from_json(self):
- """Test loading extracted data from JSON file"""
- # Create mock extracted JSON
- extracted_data = {
- "pages": [
- {
- "page_number": 1,
- "text": "Test content",
- "code_blocks": [],
- "images": []
- }
- ],
- "total_pages": 1,
- "metadata": {
- "title": "Test PDF"
- }
- }
-
- json_path = Path(self.temp_dir) / "extracted.json"
- json_path.write_text(json.dumps(extracted_data, indent=2))
-
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
- converter.load_extracted_data(str(json_path))
-
- self.assertEqual(converter.extracted_data["total_pages"], 1)
- self.assertEqual(len(converter.extracted_data["pages"]), 1)
-
- def test_build_from_json_without_extraction(self):
- """Test that from_json workflow skips PDF extraction"""
- extracted_data = {
- "pages": [{"page_number": 1, "text": "Content", "code_blocks": [], "images": []}],
- "total_pages": 1
- }
-
- json_path = Path(self.temp_dir) / "extracted.json"
- json_path.write_text(json.dumps(extracted_data))
-
- config = {
- "name": "test_skill",
- "pdf_path": "test.pdf"
- }
- converter = self.PDFToSkillConverter(config)
- converter.load_extracted_data(str(json_path))
-
- # Should have data loaded without calling extract_pdf()
- self.assertIsNotNone(converter.extracted_data)
- self.assertEqual(converter.extracted_data["total_pages"], 1)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_pr144_concerns.py b/assets/repo/Skill_Seekers-development/tests/test_pr144_concerns.py
deleted file mode 100644
index 1c3d4df..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_pr144_concerns.py
+++ /dev/null
@@ -1,149 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test script to investigate PR #144 concerns
-"""
-
-import sys
-import json
-import tempfile
-from pathlib import Path
-from collections import deque
-
-# Add cli to path
-sys.path.insert(0, str(Path(__file__).parent / 'cli'))
-
-print("="*60)
-print("PR #144 CONCERN INVESTIGATION")
-print("="*60)
-
-## CONCERN 1: Thread Safety
-print("\n1. THREAD SAFETY ANALYSIS")
-print("-" * 40)
-
-print("✓ Lock created when workers > 1:")
-print(" - Line 54-56: Creates self.lock with threading.Lock()")
-print(" - Only created when self.workers > 1")
-
-print("\n✓ Protected operations in scrape_page():")
-print(" - print() - Line 295 (with lock)")
-print(" - save_page() - Line 296 (with lock)")
-print(" - pages.append() - Line 297 (with lock)")
-print(" - visited_urls check - Line 301 (with lock)")
-print(" - pending_urls.append() - Line 302 (with lock)")
-
-print("\n✓ Protected operations in scrape_all():")
-print(" - visited_urls.add() - Line 414 (BEFORE lock!)")
-print(" - save_checkpoint() - Line 431 (with lock)")
-print(" - print() - Line 435 (with lock)")
-
-print("\n❌ RACE CONDITION FOUND:")
-print(" - Line 414: visited_urls.add(url) is OUTSIDE lock")
-print(" - Line 301: Link check 'if link not in visited_urls' is INSIDE lock")
-print(" - Two threads could add same URL to visited_urls simultaneously")
-print(" - Result: Same URL could be scraped twice")
-
-## CONCERN 2: Checkpoint Behavior
-print("\n2. CHECKPOINT WITH WORKERS")
-print("-" * 40)
-
-print("✓ Checkpoint save is protected:")
-print(" - Line 430-431: Uses lock before save_checkpoint()")
-print(" - save_checkpoint() itself does file I/O (line 103-104)")
-
-print("\n⚠️ POTENTIAL ISSUE:")
-print(" - pages_scraped counter incremented WITHOUT lock (line 427, 442)")
-print(" - Could miss checkpoints or checkpoint at wrong interval")
-print(" - Multiple threads incrementing same counter = race condition")
-
-## CONCERN 3: Error Handling
-print("\n3. ERROR HANDLING IN PARALLEL MODE")
-print("-" * 40)
-
-print("✓ Exceptions are caught in scrape_page():")
-print(" - Line 319-324: try/except wraps entire method")
-print(" - Errors are printed (with lock if workers > 1)")
-
-print("\n✓ ThreadPoolExecutor exception handling:")
-print(" - Exceptions stored in Future objects")
-print(" - as_completed() will raise exception when accessed")
-
-print("\n❌ SILENT FAILURE POSSIBLE:")
-print(" - Line 425-442: Futures are iterated but exceptions not checked")
-print(" - future.result() is never called - exceptions never raised")
-print(" - Failed pages silently disappear")
-
-## CONCERN 4: Rate Limiting Semantics
-print("\n4. RATE LIMITING WITH WORKERS")
-print("-" * 40)
-
-print("✓ Rate limit applied per-worker:")
-print(" - Line 315-317: time.sleep() after each scrape_page()")
-print(" - Each worker sleeps independently")
-
-print("\n✓ Semantics:")
-print(" - 4 workers, 0.5s rate limit = 8 requests/second total")
-print(" - 1 worker, 0.5s rate limit = 2 requests/second total")
-print(" - This is per-worker, not global rate limiting")
-
-print("\n⚠️ CONSIDERATION:")
-print(" - Documentation should clarify this is per-worker")
-print(" - Users might expect global rate limit")
-print(" - 10 workers with 0.1s = 100 req/s (very aggressive)")
-
-## CONCERN 5: Resource Limits
-print("\n5. RESOURCE LIMITS")
-print("-" * 40)
-
-print("✓ Worker limit enforced:")
-print(" - Capped at 10 workers (mentioned in PR)")
-print(" - ThreadPoolExecutor bounds threads")
-
-print("\n❌ NO MEMORY LIMITS:")
-print(" - self.pages list grows unbounded")
-print(" - visited_urls set grows unbounded")
-print(" - 10,000 pages * avg 50KB each = 500MB minimum")
-print(" - Unlimited mode could cause OOM")
-
-print("\n❌ NO PENDING URL LIMIT:")
-print(" - pending_urls deque grows unbounded")
-print(" - Could have thousands of URLs queued")
-
-## CONCERN 6: Streaming Subprocess
-print("\n6. STREAMING SUBPROCESS")
-print("-" * 40)
-
-print("✓ Good implementation:")
-print(" - Uses select() for non-blocking I/O")
-print(" - Timeout mechanism works (line 60-63)")
-print(" - Kills process on timeout")
-
-print("\n⚠️ Windows fallback:")
-print(" - Line 83-85: Falls back to sleep() on Windows")
-print(" - Won't stream output on Windows (will appear frozen)")
-print(" - But will still work, just poor UX")
-
-print("\n✓ Process cleanup:")
-print(" - Line 88: communicate() gets remaining output")
-print(" - process.returncode properly captured")
-
-print("\n" + "="*60)
-print("SUMMARY OF FINDINGS")
-print("="*60)
-
-print("\n🚨 CRITICAL ISSUES FOUND:")
-print("1. Race condition on visited_urls.add() (line 414)")
-print("2. pages_scraped counter not thread-safe")
-print("3. Silent exception swallowing in parallel mode")
-
-print("\n⚠️ MODERATE CONCERNS:")
-print("4. No memory limits for unlimited mode")
-print("5. Per-worker rate limiting may confuse users")
-print("6. Windows streaming falls back to polling")
-
-print("\n✅ WORKS CORRECTLY:")
-print("7. Lock protects most shared state")
-print("8. Checkpoint saves are protected")
-print("9. save_page() file I/O protected")
-print("10. Timeout mechanism solid")
-
-print("\n" + "="*60)
diff --git a/assets/repo/Skill_Seekers-development/tests/test_quality_checker.py b/assets/repo/Skill_Seekers-development/tests/test_quality_checker.py
deleted file mode 100644
index 104d5b9..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_quality_checker.py
+++ /dev/null
@@ -1,297 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for cli/quality_checker.py functionality
-"""
-
-import unittest
-import tempfile
-from pathlib import Path
-import os
-
-from skill_seekers.cli.quality_checker import SkillQualityChecker, QualityReport
-
-
-class TestQualityChecker(unittest.TestCase):
- """Test quality checker functionality"""
-
- def create_test_skill(self, tmpdir, skill_md_content, create_references=True):
- """Helper to create a test skill directory"""
- skill_dir = Path(tmpdir) / "test-skill"
- skill_dir.mkdir()
-
- # Create SKILL.md
- skill_md = skill_dir / "SKILL.md"
- skill_md.write_text(skill_md_content, encoding='utf-8')
-
- # Create references directory
- if create_references:
- refs_dir = skill_dir / "references"
- refs_dir.mkdir()
- (refs_dir / "index.md").write_text("# Index\n\nTest reference.", encoding='utf-8')
- (refs_dir / "getting_started.md").write_text("# Getting Started\n\nHow to start.", encoding='utf-8')
-
- return skill_dir
-
- def test_checker_detects_missing_skill_md(self):
- """Test that checker detects missing SKILL.md"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = Path(tmpdir) / "test-skill"
- skill_dir.mkdir()
-
- checker = SkillQualityChecker(skill_dir)
- report = checker.check_all()
-
- # Should have error about missing SKILL.md
- self.assertTrue(report.has_errors)
- self.assertTrue(any('SKILL.md' in issue.message for issue in report.errors))
-
- def test_checker_detects_missing_references(self):
- """Test that checker warns about missing references"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_md = """---
-name: test
----
-
-# Test Skill
-
-This is a test.
-"""
- skill_dir = self.create_test_skill(tmpdir, skill_md, create_references=False)
-
- checker = SkillQualityChecker(skill_dir)
- report = checker.check_all()
-
- # Should have warning about missing references
- self.assertTrue(report.has_warnings)
- self.assertTrue(any('references' in issue.message.lower() for issue in report.warnings))
-
- def test_checker_detects_invalid_frontmatter(self):
- """Test that checker detects invalid YAML frontmatter"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_md = """# Test Skill
-
-No frontmatter here!
-"""
- skill_dir = self.create_test_skill(tmpdir, skill_md)
-
- checker = SkillQualityChecker(skill_dir)
- report = checker.check_all()
-
- # Should have error about missing frontmatter
- self.assertTrue(report.has_errors)
- self.assertTrue(any('frontmatter' in issue.message.lower() for issue in report.errors))
-
- def test_checker_detects_missing_name_field(self):
- """Test that checker detects missing name field in frontmatter"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_md = """---
-description: test
----
-
-# Test Skill
-"""
- skill_dir = self.create_test_skill(tmpdir, skill_md)
-
- checker = SkillQualityChecker(skill_dir)
- report = checker.check_all()
-
- # Should have error about missing name field
- self.assertTrue(report.has_errors)
- self.assertTrue(any('name' in issue.message.lower() for issue in report.errors))
-
- def test_checker_detects_code_without_language(self):
- """Test that checker warns about code blocks without language tags"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_md = """---
-name: test
----
-
-# Test Skill
-
-Here's some code:
-
-```
-print("hello")
-```
-"""
- skill_dir = self.create_test_skill(tmpdir, skill_md)
-
- checker = SkillQualityChecker(skill_dir)
- report = checker.check_all()
-
- # Should have warning about code without language
- self.assertTrue(report.has_warnings)
- self.assertTrue(any('language' in issue.message.lower() for issue in report.warnings))
-
- def test_checker_approves_good_skill(self):
- """Test that checker gives high score to well-formed skill"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_md = """---
-name: test
-description: A test skill
----
-
-# Test Skill
-
-## When to Use This Skill
-
-Use this when you need to test.
-
-## Quick Reference
-
-Here are some examples:
-
-```python
-def hello():
- print("hello")
-```
-
-```javascript
-console.log("hello");
-```
-
-## Example: Basic Usage
-
-This shows how to use it.
-
-## Reference Files
-
-See the references directory for more:
-- [Getting Started](references/getting_started.md)
-- [Index](references/index.md)
-"""
- skill_dir = self.create_test_skill(tmpdir, skill_md)
-
- checker = SkillQualityChecker(skill_dir)
- report = checker.check_all()
-
- # Should have no errors
- self.assertFalse(report.has_errors)
-
- # Quality score should be high
- self.assertGreaterEqual(report.quality_score, 80.0)
-
- def test_checker_detects_broken_links(self):
- """Test that checker detects broken internal links"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_md = """---
-name: test
----
-
-# Test Skill
-
-See [this file](nonexistent.md) for more info.
-"""
- skill_dir = self.create_test_skill(tmpdir, skill_md)
-
- checker = SkillQualityChecker(skill_dir)
- report = checker.check_all()
-
- # Should have warning about broken link
- self.assertTrue(report.has_warnings)
- self.assertTrue(any('broken link' in issue.message.lower() for issue in report.warnings))
-
- def test_quality_score_calculation(self):
- """Test that quality score is calculated correctly"""
- with tempfile.TemporaryDirectory() as tmpdir:
- report = QualityReport("test", Path(tmpdir))
-
- # Perfect score to start
- self.assertEqual(report.quality_score, 100.0)
-
- # Add an error (should deduct 15 points)
- report.add_error('test', 'Test error')
- self.assertEqual(report.quality_score, 85.0)
-
- # Add a warning (should deduct 5 points)
- report.add_warning('test', 'Test warning')
- self.assertEqual(report.quality_score, 80.0)
-
- # Add more errors
- report.add_error('test', 'Another error')
- report.add_error('test', 'Yet another error')
- self.assertEqual(report.quality_score, 50.0)
-
- def test_quality_grade_calculation(self):
- """Test that quality grades are assigned correctly"""
- with tempfile.TemporaryDirectory() as tmpdir:
- report = QualityReport("test", Path(tmpdir))
-
- # Grade A (90-100)
- self.assertEqual(report.quality_grade, 'A')
-
- # Grade B (80-89)
- report.add_error('test', 'Error 1')
- self.assertEqual(report.quality_grade, 'B')
-
- # Grade C (70-79)
- report.add_warning('test', 'Warning 1')
- report.add_warning('test', 'Warning 2')
- self.assertEqual(report.quality_grade, 'C')
-
- # Grade D (60-69)
- report.add_warning('test', 'Warning 3')
- report.add_warning('test', 'Warning 4')
- self.assertEqual(report.quality_grade, 'D')
-
- # Grade F (below 60)
- report.add_error('test', 'Error 2')
- report.add_error('test', 'Error 3')
- self.assertEqual(report.quality_grade, 'F')
-
- def test_is_excellent_property(self):
- """Test is_excellent property"""
- with tempfile.TemporaryDirectory() as tmpdir:
- report = QualityReport("test", Path(tmpdir))
-
- # Should be excellent with no issues
- self.assertTrue(report.is_excellent)
-
- # Adding an error should make it not excellent
- report.add_error('test', 'Test error')
- self.assertFalse(report.is_excellent)
-
- # Clean report
- report2 = QualityReport("test", Path(tmpdir))
- # Adding a warning should also make it not excellent
- report2.add_warning('test', 'Test warning')
- self.assertFalse(report2.is_excellent)
-
-
-class TestQualityCheckerCLI(unittest.TestCase):
- """Test quality checker CLI"""
-
- def test_cli_help_output(self):
- """Test that CLI help works"""
- import subprocess
-
- try:
- result = subprocess.run(
- ['python3', '-m', 'skill_seekers.cli.quality_checker', '--help'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # Should include usage info
- output = result.stdout + result.stderr
- self.assertTrue('usage:' in output.lower() or 'quality' in output.lower())
- except FileNotFoundError:
- self.skipTest("Module not installed")
-
- def test_cli_with_nonexistent_directory(self):
- """Test CLI behavior with nonexistent directory"""
- import subprocess
-
- result = subprocess.run(
- ['python3', '-m', 'skill_seekers.cli.quality_checker', '/nonexistent/path'],
- capture_output=True,
- text=True
- )
-
- # Should fail
- self.assertNotEqual(result.returncode, 0)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_scraper_features.py b/assets/repo/Skill_Seekers-development/tests/test_scraper_features.py
deleted file mode 100644
index a4b789e..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_scraper_features.py
+++ /dev/null
@@ -1,527 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test suite for doc_scraper core features
-Tests URL validation, language detection, pattern extraction, and categorization
-"""
-
-import sys
-import os
-import unittest
-from unittest.mock import Mock, MagicMock
-from bs4 import BeautifulSoup
-
-# Add parent directory to path
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.doc_scraper import DocToSkillConverter
-
-
-class TestURLValidation(unittest.TestCase):
- """Test URL validation logic"""
-
- def setUp(self):
- """Set up test converter"""
- self.config = {
- 'name': 'test',
- 'base_url': 'https://docs.example.com/',
- 'url_patterns': {
- 'include': ['/guide/', '/api/'],
- 'exclude': ['/blog/', '/about/']
- },
- 'selectors': {
- 'main_content': 'article',
- 'title': 'h1',
- 'code_blocks': 'pre code'
- },
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- self.converter = DocToSkillConverter(self.config, dry_run=True)
-
- def test_valid_url_with_include_pattern(self):
- """Test URL matching include pattern"""
- url = 'https://docs.example.com/guide/getting-started'
- self.assertTrue(self.converter.is_valid_url(url))
-
- def test_valid_url_with_api_pattern(self):
- """Test URL matching API pattern"""
- url = 'https://docs.example.com/api/reference'
- self.assertTrue(self.converter.is_valid_url(url))
-
- def test_invalid_url_with_exclude_pattern(self):
- """Test URL matching exclude pattern"""
- url = 'https://docs.example.com/blog/announcement'
- self.assertFalse(self.converter.is_valid_url(url))
-
- def test_invalid_url_different_domain(self):
- """Test URL from different domain"""
- url = 'https://other-site.com/guide/tutorial'
- self.assertFalse(self.converter.is_valid_url(url))
-
- def test_invalid_url_no_include_match(self):
- """Test URL not matching any include pattern"""
- url = 'https://docs.example.com/download/installer'
- self.assertFalse(self.converter.is_valid_url(url))
-
- def test_url_validation_no_patterns(self):
- """Test URL validation with no include/exclude patterns"""
- config = {
- 'name': 'test',
- 'base_url': 'https://docs.example.com/',
- 'url_patterns': {
- 'include': [],
- 'exclude': []
- },
- 'selectors': {'main_content': 'article', 'title': 'h1', 'code_blocks': 'pre'},
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- converter = DocToSkillConverter(config, dry_run=True)
-
- # Should accept any URL under base_url
- self.assertTrue(converter.is_valid_url('https://docs.example.com/anything'))
- self.assertFalse(converter.is_valid_url('https://other.com/anything'))
-
-
-class TestLanguageDetection(unittest.TestCase):
- """Test language detection from code blocks"""
-
- def setUp(self):
- """Set up test converter"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article', 'title': 'h1', 'code_blocks': 'pre'},
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- self.converter = DocToSkillConverter(config, dry_run=True)
-
- def test_detect_language_from_class(self):
- """Test language detection from CSS class"""
- html = 'print("hello")'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- lang = self.converter.detect_language(elem, 'print("hello")')
- self.assertEqual(lang, 'python')
-
- def test_detect_language_from_lang_class(self):
- """Test language detection from lang- prefix"""
- html = 'console.log("hello")'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- lang = self.converter.detect_language(elem, 'console.log("hello")')
- self.assertEqual(lang, 'javascript')
-
- def test_detect_language_from_parent(self):
- """Test language detection from parent pre element"""
- html = 'int main() {}
'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- lang = self.converter.detect_language(elem, 'int main() {}')
- self.assertEqual(lang, 'cpp')
-
- def test_detect_python_from_heuristics(self):
- """Test Python detection from code content"""
- html = 'import os\nfrom pathlib import Path'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'python')
-
- def test_detect_python_from_def(self):
- """Test Python detection from def keyword"""
- html = 'def my_function():\n pass'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'python')
-
- def test_detect_javascript_from_const(self):
- """Test JavaScript detection from const keyword"""
- html = 'const myVar = 10;'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'javascript')
-
- def test_detect_javascript_from_arrow(self):
- """Test JavaScript detection from arrow function"""
- html = 'const add = (a, b) => a + b;'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'javascript')
-
- def test_detect_gdscript(self):
- """Test GDScript detection"""
- html = 'func _ready():\n var x = 5'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'gdscript')
-
- def test_detect_cpp(self):
- """Test C++ detection"""
- html = '#include \nint main() { return 0; } '
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'cpp')
-
- def test_detect_unknown(self):
- """Test unknown language detection"""
- html = 'some random text without clear indicators'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'unknown')
-
- def test_detect_brush_pattern_in_pre(self):
- """Test brush: pattern in pre element"""
- html = 'x
'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- lang = self.converter.detect_language(elem, 'x')
- self.assertEqual(lang, 'python', 'Should detect python from brush: python pattern')
-
- def test_detect_bare_class_in_pre(self):
- """Test bare class name in pre element"""
- html = 'x
'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- lang = self.converter.detect_language(elem, 'x')
- self.assertEqual(lang, 'python', 'Should detect python from bare class name')
-
- def test_detect_bare_class_in_code(self):
- """Test bare class name in code element"""
- html = 'x'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- lang = self.converter.detect_language(elem, 'x')
- self.assertEqual(lang, 'python', 'Should detect python from bare class name')
-
- def test_detect_csharp_from_using_system(self):
- """Test C# detection from 'using System' keyword"""
- html = 'using System;\nnamespace MyApp { }'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'csharp', 'Should detect C# from using System')
-
- def test_detect_csharp_from_namespace(self):
- """Test C# detection from 'namespace' keyword"""
- html = 'namespace MyNamespace\n{\n public class Test { }\n}'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'csharp', 'Should detect C# from namespace')
-
- def test_detect_csharp_from_property_syntax(self):
- """Test C# detection from property syntax"""
- html = 'public string Name { get; set; }'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'csharp', 'Should detect C# from { get; set; } syntax')
-
- def test_detect_csharp_from_public_class(self):
- """Test C# detection from 'public class' keyword"""
- html = 'public class MyClass\n{\n private int value;\n}'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'csharp', 'Should detect C# from public class')
-
- def test_detect_csharp_from_private_class(self):
- """Test C# detection from 'private class' keyword"""
- html = 'private class Helper { }'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'csharp', 'Should detect C# from private class')
-
- def test_detect_csharp_from_public_static_void(self):
- """Test C# detection from 'public static void' keyword"""
- html = 'public static void Main(string[] args)\n{\n Console.WriteLine("Test");\n}'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'csharp', 'Should detect C# from public static void')
-
- def test_detect_csharp_from_class_attribute(self):
- """Test C# detection from CSS class attribute"""
- html = 'var x = 5;'
- elem = BeautifulSoup(html, 'html.parser').find('code')
- code = elem.get_text()
- lang = self.converter.detect_language(elem, code)
- self.assertEqual(lang, 'csharp', 'Should detect C# from language-csharp class')
-
-
-class TestPatternExtraction(unittest.TestCase):
- """Test pattern extraction from documentation"""
-
- def setUp(self):
- """Set up test converter"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article', 'title': 'h1', 'code_blocks': 'pre'},
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- self.converter = DocToSkillConverter(config, dry_run=True)
-
- def test_extract_pattern_with_example_marker(self):
- """Test pattern extraction with 'Example:' marker"""
- html = '''
-
- Example: Here's how to use it
- print("hello")
-
- '''
- soup = BeautifulSoup(html, 'html.parser')
- main = soup.find('article')
- patterns = self.converter.extract_patterns(main, [])
-
- self.assertGreater(len(patterns), 0)
- self.assertIn('example', patterns[0]['description'].lower())
-
- def test_extract_pattern_with_usage_marker(self):
- """Test pattern extraction with 'Usage:' marker"""
- html = '''
-
- Usage: Call this function like so
- my_function(arg)
-
- '''
- soup = BeautifulSoup(html, 'html.parser')
- main = soup.find('article')
- patterns = self.converter.extract_patterns(main, [])
-
- self.assertGreater(len(patterns), 0)
- self.assertIn('usage', patterns[0]['description'].lower())
-
- def test_extract_pattern_limit(self):
- """Test pattern extraction limits to 5 patterns"""
- html = ''
- for i in range(10):
- html += f'Example {i}: Test
code_{i}
'
- html += ' '
-
- soup = BeautifulSoup(html, 'html.parser')
- main = soup.find('article')
- patterns = self.converter.extract_patterns(main, [])
-
- self.assertLessEqual(len(patterns), 5, "Should limit to 5 patterns max")
-
-
-class TestCategorization(unittest.TestCase):
- """Test smart categorization logic"""
-
- def setUp(self):
- """Set up test converter"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'categories': {
- 'getting_started': ['intro', 'tutorial', 'getting-started'],
- 'api': ['api', 'reference', 'class'],
- 'guides': ['guide', 'how-to']
- },
- 'selectors': {'main_content': 'article', 'title': 'h1', 'code_blocks': 'pre'},
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- self.converter = DocToSkillConverter(config, dry_run=True)
-
- def test_categorize_by_url(self):
- """Test categorization based on URL"""
- pages = [{
- 'url': 'https://example.com/api/reference',
- 'title': 'Some Title',
- 'content': 'Some content'
- }]
- categories = self.converter.smart_categorize(pages)
-
- # Should categorize to 'api' based on URL containing 'api'
- self.assertIn('api', categories)
- self.assertEqual(len(categories['api']), 1)
-
- def test_categorize_by_title(self):
- """Test categorization based on title"""
- pages = [{
- 'url': 'https://example.com/docs/page',
- 'title': 'API Reference Documentation',
- 'content': 'Some content'
- }]
- categories = self.converter.smart_categorize(pages)
-
- self.assertIn('api', categories)
- self.assertEqual(len(categories['api']), 1)
-
- def test_categorize_by_content(self):
- """Test categorization based on content (lower priority)"""
- pages = [{
- 'url': 'https://example.com/docs/page',
- 'title': 'Some Page',
- 'content': 'This is a tutorial for beginners. An intro to the system.'
- }]
- categories = self.converter.smart_categorize(pages)
-
- # Should categorize based on 'tutorial' and 'intro' in content
- self.assertIn('getting_started', categories)
-
- def test_categorize_to_other(self):
- """Test pages that don't match any category go to 'other'"""
- pages = [{
- 'url': 'https://example.com/random/page',
- 'title': 'Random Page',
- 'content': 'Random content with no keywords'
- }]
- categories = self.converter.smart_categorize(pages)
-
- self.assertIn('other', categories)
- self.assertEqual(len(categories['other']), 1)
-
- def test_empty_categories_removed(self):
- """Test empty categories are removed"""
- pages = [{
- 'url': 'https://example.com/api/reference',
- 'title': 'API Reference',
- 'content': 'API documentation'
- }]
- categories = self.converter.smart_categorize(pages)
-
- # Only 'api' should exist, not empty 'guides' or 'getting_started'
- # (categories with no pages are removed)
- self.assertIn('api', categories)
- self.assertNotIn('guides', categories)
-
-
-class TestLinkExtraction(unittest.TestCase):
- """Test link extraction and anchor fragment handling"""
-
- def setUp(self):
- """Set up test converter"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article', 'title': 'h1', 'code_blocks': 'pre code'},
- 'url_patterns': {
- 'include': [],
- 'exclude': []
- },
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- self.converter = DocToSkillConverter(config, dry_run=True)
-
- def test_extract_links_strips_anchor_fragments(self):
- """Test that anchor fragments (#anchor) are stripped from extracted links"""
- html = '''
-
- Test Page
- Content with links
- Link 1
- Link 2
- Link 3
-
- '''
- soup = BeautifulSoup(html, 'html.parser')
- page = self.converter.extract_content(soup, 'https://example.com/')
-
- # Should have 2 unique URLs (page.html and other.html), not 3
- # The two links with different anchors should be deduplicated
- self.assertEqual(len(page['links']), 2)
- self.assertIn('https://example.com/docs/page.html', page['links'])
- self.assertIn('https://example.com/docs/other.html', page['links'])
-
- def test_extract_links_no_anchor_duplicates(self):
- """Test that multiple anchor links to same page don't create duplicates"""
- html = '''
-
- Test Page
- Anchor 1
- Anchor 2
- Anchor 3
- Anchor 4
- Anchor 5
-
- '''
- soup = BeautifulSoup(html, 'html.parser')
- page = self.converter.extract_content(soup, 'https://example.com/')
-
- # All 5 links point to the same page, should result in only 1 URL
- self.assertEqual(len(page['links']), 1)
- self.assertEqual(page['links'][0], 'https://example.com/docs/api.html')
-
- def test_extract_links_preserves_query_params(self):
- """Test that query parameters are preserved when stripping anchors"""
- html = '''
-
- Test Page
- Search Result
-
- '''
- soup = BeautifulSoup(html, 'html.parser')
- page = self.converter.extract_content(soup, 'https://example.com/')
-
- # Query params should be preserved, only anchor stripped
- self.assertEqual(len(page['links']), 1)
- self.assertEqual(page['links'][0], 'https://example.com/search?q=test')
-
- def test_extract_links_relative_urls_with_anchors(self):
- """Test that relative URLs with anchors are handled correctly"""
- html = '''
-
- Test Page
- Relative Link 1
- Relative Link 2
- Relative Link 3
-
- '''
- soup = BeautifulSoup(html, 'html.parser')
- page = self.converter.extract_content(soup, 'https://example.com/')
-
- # Should have 2 unique URLs (guide.html and tutorial.html)
- self.assertEqual(len(page['links']), 2)
- self.assertIn('https://example.com/docs/guide.html', page['links'])
- self.assertIn('https://example.com/docs/tutorial.html', page['links'])
-
-
-class TestTextCleaning(unittest.TestCase):
- """Test text cleaning utility"""
-
- def setUp(self):
- """Set up test converter"""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article', 'title': 'h1', 'code_blocks': 'pre'},
- 'rate_limit': 0.1,
- 'max_pages': 10
- }
- self.converter = DocToSkillConverter(config, dry_run=True)
-
- def test_clean_multiple_spaces(self):
- """Test cleaning multiple spaces"""
- text = "Hello world test"
- cleaned = self.converter.clean_text(text)
- self.assertEqual(cleaned, "Hello world test")
-
- def test_clean_newlines(self):
- """Test cleaning newlines"""
- text = "Hello\n\nworld\ntest"
- cleaned = self.converter.clean_text(text)
- self.assertEqual(cleaned, "Hello world test")
-
- def test_clean_tabs(self):
- """Test cleaning tabs"""
- text = "Hello\t\tworld\ttest"
- cleaned = self.converter.clean_text(text)
- self.assertEqual(cleaned, "Hello world test")
-
- def test_clean_strip_whitespace(self):
- """Test stripping leading/trailing whitespace"""
- text = " Hello world "
- cleaned = self.converter.clean_text(text)
- self.assertEqual(cleaned, "Hello world")
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_setup_scripts.py b/assets/repo/Skill_Seekers-development/tests/test_setup_scripts.py
deleted file mode 100644
index afd3764..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_setup_scripts.py
+++ /dev/null
@@ -1,241 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test setup scripts for correctness and path validation.
-
-Tests that bash scripts reference correct paths and are syntactically valid.
-"""
-
-import subprocess
-import re
-from pathlib import Path
-import pytest
-
-
-class TestSetupMCPScript:
- """Test setup_mcp.sh for path correctness and syntax"""
-
- @pytest.fixture
- def script_path(self):
- """Get path to setup_mcp.sh"""
- return Path("setup_mcp.sh")
-
- @pytest.fixture
- def script_content(self, script_path):
- """Read setup_mcp.sh content"""
- with open(script_path, 'r') as f:
- return f.read()
-
- def test_setup_mcp_exists(self, script_path):
- """Test that setup_mcp.sh exists"""
- assert script_path.exists(), "setup_mcp.sh should exist"
- assert script_path.is_file(), "setup_mcp.sh should be a file"
-
- def test_bash_syntax_valid(self, script_path):
- """Test that setup_mcp.sh has valid bash syntax"""
- result = subprocess.run(
- ["bash", "-n", str(script_path)],
- capture_output=True,
- text=True
- )
- assert result.returncode == 0, f"Bash syntax error: {result.stderr}"
-
- def test_references_correct_mcp_directory(self, script_content):
- """Test that script references src/skill_seekers/mcp/ (v2.0.0 layout)"""
- # Should NOT reference old mcp/ or skill_seeker_mcp/ directories
- old_mcp_refs = re.findall(r'(?:^|[^a-z_])(?= 6, f"Expected at least 6 references to 'src/skill_seekers/mcp/', found {len(new_refs)}"
-
- def test_requirements_txt_path(self, script_content):
- """Test that script uses pip install -e . (v2.0.0 modern packaging)"""
- # v2.0.0 uses '-e .' (editable install) instead of requirements files
- # The actual command is "$PIP_INSTALL_CMD -e ."
- assert " -e ." in script_content or " -e." in script_content, \
- "Should use '-e .' for editable install (modern packaging)"
-
- # Should NOT reference old requirements.txt paths
- import re
- old_skill_seeker_refs = re.findall(r'skill_seeker_mcp/requirements\.txt', script_content)
- old_mcp_refs = re.findall(r'(? 0:
- pytest.fail(f"README references old mcp/ directory: {old_mcp_refs}")
-
- def test_documentation_references_correct_paths(self):
- """Test that documentation files reference correct MCP paths"""
- doc_files = list(Path("docs/").glob("*.md")) if Path("docs/").exists() else []
- for doc_file in doc_files:
- with open(doc_file, 'r') as f:
- content = f.read()
- # Check for old mcp/ directory paths (but allow mcp.json and "mcp" package name)
- old_mcp_refs = re.findall(r'(? 0:
- pytest.fail(f"{doc_file} references old mcp/ directory: {old_mcp_refs}")
-
-
-def test_mcp_directory_structure():
- """Test that MCP directory structure is correct (new src/ layout)"""
- mcp_dir = Path("src/skill_seekers/mcp")
- assert mcp_dir.exists(), "src/skill_seekers/mcp/ directory should exist"
- assert mcp_dir.is_dir(), "src/skill_seekers/mcp should be a directory"
- assert (mcp_dir / "server.py").exists(), "src/skill_seekers/mcp/server.py should exist"
- assert (mcp_dir / "__init__.py").exists(), "src/skill_seekers/mcp/__init__.py should exist"
-
- # Old directories should NOT exist
- old_mcp = Path("mcp")
- old_skill_seeker_mcp = Path("skill_seeker_mcp")
- if old_mcp.exists():
- # If it exists, it should not contain server.py (might be leftover empty dir)
- assert not (old_mcp / "server.py").exists(), \
- "Old mcp/server.py should not exist - migrated to src/skill_seekers/mcp/"
- if old_skill_seeker_mcp.exists():
- assert not (old_skill_seeker_mcp / "server.py").exists(), \
- "Old skill_seeker_mcp/server.py should not exist - migrated to src/skill_seekers/mcp/"
-
-
-if __name__ == '__main__':
- print("=" * 60)
- print("Testing Setup Scripts")
- print("=" * 60)
- pytest.main([__file__, "-v"])
diff --git a/assets/repo/Skill_Seekers-development/tests/test_skip_llms_txt.py b/assets/repo/Skill_Seekers-development/tests/test_skip_llms_txt.py
deleted file mode 100644
index f863b43..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_skip_llms_txt.py
+++ /dev/null
@@ -1,318 +0,0 @@
-"""Tests for skip_llms_txt configuration option.
-
-This config option allows users to explicitly skip llms.txt detection and fetching,
-which is useful when:
-- A site's llms.txt is incomplete or incorrect
-- You need specific pages not in llms.txt
-- You want to force HTML scraping
-"""
-
-import os
-import tempfile
-import unittest
-import logging
-from unittest.mock import patch, Mock, MagicMock
-
-from skill_seekers.cli.doc_scraper import DocToSkillConverter
-
-
-class TestSkipLlmsTxtConfig(unittest.TestCase):
- """Test skip_llms_txt configuration option."""
-
- def test_default_skip_llms_txt_is_false(self):
- """Test that skip_llms_txt defaults to False when not specified."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'}
- }
-
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertFalse(converter.skip_llms_txt)
-
- def test_skip_llms_txt_can_be_set_true(self):
- """Test that skip_llms_txt can be explicitly set to True."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': True
- }
-
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertTrue(converter.skip_llms_txt)
-
- def test_skip_llms_txt_can_be_set_false(self):
- """Test that skip_llms_txt can be explicitly set to False."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': False
- }
-
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertFalse(converter.skip_llms_txt)
-
-
-class TestSkipLlmsTxtSyncBehavior(unittest.TestCase):
- """Test skip_llms_txt behavior in sync scraping mode."""
-
- def test_llms_txt_tried_when_not_skipped(self):
- """Test that _try_llms_txt is called when skip_llms_txt is False."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': False
- }
-
- original_cwd = os.getcwd()
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=False)
-
- with patch.object(converter, '_try_llms_txt', return_value=False) as mock_try:
- with patch.object(converter, 'scrape_page'):
- with patch.object(converter, 'save_summary'):
- converter.scrape_all()
- mock_try.assert_called_once()
- finally:
- os.chdir(original_cwd)
-
- def test_llms_txt_skipped_when_skip_true(self):
- """Test that _try_llms_txt is NOT called when skip_llms_txt is True."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': True
- }
-
- original_cwd = os.getcwd()
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=False)
-
- with patch.object(converter, '_try_llms_txt') as mock_try:
- with patch.object(converter, 'scrape_page'):
- with patch.object(converter, 'save_summary'):
- converter.scrape_all()
- mock_try.assert_not_called()
- finally:
- os.chdir(original_cwd)
-
- def test_llms_txt_skipped_in_dry_run_mode(self):
- """Test that _try_llms_txt is NOT called in dry-run mode regardless of skip setting."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': False # Even when False
- }
-
- original_cwd = os.getcwd()
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=True)
-
- with patch.object(converter, '_try_llms_txt') as mock_try:
- with patch.object(converter, 'save_summary'):
- converter.scrape_all()
- mock_try.assert_not_called()
- finally:
- os.chdir(original_cwd)
-
-
-class TestSkipLlmsTxtAsyncBehavior(unittest.TestCase):
- """Test skip_llms_txt behavior in async scraping mode."""
-
- def test_async_llms_txt_tried_when_not_skipped(self):
- """Test that _try_llms_txt is called in async mode when skip_llms_txt is False."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'async_mode': True,
- 'skip_llms_txt': False
- }
-
- original_cwd = os.getcwd()
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=False)
-
- with patch.object(converter, '_try_llms_txt', return_value=False) as mock_try:
- with patch.object(converter, 'scrape_page_async', return_value=None):
- with patch.object(converter, 'save_summary'):
- converter.scrape_all()
- mock_try.assert_called_once()
- finally:
- os.chdir(original_cwd)
-
- def test_async_llms_txt_skipped_when_skip_true(self):
- """Test that _try_llms_txt is NOT called in async mode when skip_llms_txt is True."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'async_mode': True,
- 'skip_llms_txt': True
- }
-
- original_cwd = os.getcwd()
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=False)
-
- with patch.object(converter, '_try_llms_txt') as mock_try:
- with patch.object(converter, 'scrape_page_async', return_value=None):
- with patch.object(converter, 'save_summary'):
- converter.scrape_all()
- mock_try.assert_not_called()
- finally:
- os.chdir(original_cwd)
-
-
-class TestSkipLlmsTxtWithRealConfig(unittest.TestCase):
- """Test skip_llms_txt with real-world config patterns."""
-
- def test_telegram_bots_config_pattern(self):
- """Test the telegram-bots config pattern which uses skip_llms_txt."""
- config = {
- 'name': 'telegram-bots',
- 'description': 'Telegram bot documentation',
- 'base_url': 'https://core.telegram.org/bots',
- 'skip_llms_txt': True, # Telegram doesn't have useful llms.txt
- 'start_urls': [
- 'https://core.telegram.org/bots',
- 'https://core.telegram.org/bots/api'
- ],
- 'selectors': {
- 'main_content': '#dev_page_content, main, article',
- 'title': 'h1, title',
- 'code_blocks': 'pre code, pre'
- }
- }
-
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertTrue(converter.skip_llms_txt)
- self.assertEqual(converter.name, 'telegram-bots')
-
- def test_skip_llms_txt_with_multiple_start_urls(self):
- """Test skip_llms_txt works correctly with multiple start URLs."""
- config = {
- 'name': 'test-multi',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': True,
- 'start_urls': [
- 'https://example.com/docs/',
- 'https://example.com/api/',
- 'https://example.com/guide/'
- ]
- }
-
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertTrue(converter.skip_llms_txt)
- # start_urls are stored in pending_urls deque
- self.assertEqual(len(converter.pending_urls), 3)
-
-
-class TestSkipLlmsTxtEdgeCases(unittest.TestCase):
- """Test edge cases for skip_llms_txt."""
-
- def test_skip_llms_txt_with_int_zero_logs_warning(self):
- """Test that integer 0 logs warning and defaults to False."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': 0 # Invalid type
- }
-
- with self.assertLogs('skill_seekers.cli.doc_scraper', level='WARNING') as cm:
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertFalse(converter.skip_llms_txt)
- self.assertTrue(any('Invalid value' in log and '0' in log for log in cm.output))
-
- def test_skip_llms_txt_with_int_one_logs_warning(self):
- """Test that integer 1 logs warning and defaults to False."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': 1 # Invalid type
- }
-
- with self.assertLogs('skill_seekers.cli.doc_scraper', level='WARNING') as cm:
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertFalse(converter.skip_llms_txt)
- self.assertTrue(any('Invalid value' in log and '1' in log for log in cm.output))
-
- def test_skip_llms_txt_with_string_logs_warning(self):
- """Test that string values log warning and default to False."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': "true" # Invalid type
- }
-
- with self.assertLogs('skill_seekers.cli.doc_scraper', level='WARNING') as cm:
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertFalse(converter.skip_llms_txt)
- self.assertTrue(any('Invalid value' in log and 'true' in log for log in cm.output))
-
- def test_skip_llms_txt_with_none_logs_warning(self):
- """Test that None logs warning and defaults to False."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': None # Invalid type
- }
-
- with self.assertLogs('skill_seekers.cli.doc_scraper', level='WARNING') as cm:
- converter = DocToSkillConverter(config, dry_run=True)
- self.assertFalse(converter.skip_llms_txt)
- self.assertTrue(any('Invalid value' in log and 'None' in log for log in cm.output))
-
- def test_scraping_proceeds_when_llms_txt_skipped(self):
- """Test that HTML scraping proceeds normally when llms.txt is skipped."""
- config = {
- 'name': 'test',
- 'base_url': 'https://example.com/',
- 'selectors': {'main_content': 'article'},
- 'skip_llms_txt': True
- }
-
- original_cwd = os.getcwd()
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- os.chdir(tmpdir)
- converter = DocToSkillConverter(config, dry_run=False)
-
- # Track if scrape_page was called
- scrape_called = []
-
- def mock_scrape(url):
- scrape_called.append(url)
- return None
-
- with patch.object(converter, 'scrape_page', side_effect=mock_scrape):
- with patch.object(converter, 'save_summary'):
- converter.scrape_all()
- # Should have attempted to scrape the base URL
- self.assertTrue(len(scrape_called) > 0)
- finally:
- os.chdir(original_cwd)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_terminal_detection.py b/assets/repo/Skill_Seekers-development/tests/test_terminal_detection.py
deleted file mode 100644
index 690ed7f..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_terminal_detection.py
+++ /dev/null
@@ -1,332 +0,0 @@
-"""
-Tests for terminal detection functionality in enhance_skill_local.py
-
-This module tests the detect_terminal_app() function and terminal launching logic
-to ensure correct terminal selection across different environments.
-"""
-
-import unittest
-import os
-import sys
-from unittest.mock import patch, MagicMock
-from pathlib import Path
-
-# Add parent directory to path for imports
-sys.path.insert(0, str(Path(__file__).parent.parent))
-
-from skill_seekers.cli.enhance_skill_local import detect_terminal_app, LocalSkillEnhancer
-
-
-class TestDetectTerminalApp(unittest.TestCase):
- """Test the detect_terminal_app() function."""
-
- original_skill_seeker: str | None = None
- original_term_program: str | None = None
-
- def setUp(self):
- """Save original environment variables."""
- self.original_skill_seeker = os.environ.get('SKILL_SEEKER_TERMINAL')
- self.original_term_program = os.environ.get('TERM_PROGRAM')
-
- def tearDown(self):
- """Restore original environment variables."""
- # Remove test env vars
- if 'SKILL_SEEKER_TERMINAL' in os.environ:
- del os.environ['SKILL_SEEKER_TERMINAL']
- if 'TERM_PROGRAM' in os.environ:
- del os.environ['TERM_PROGRAM']
-
- # Restore originals if they existed
- if self.original_skill_seeker is not None:
- os.environ['SKILL_SEEKER_TERMINAL'] = self.original_skill_seeker
- if self.original_term_program is not None:
- os.environ['TERM_PROGRAM'] = self.original_term_program
-
- # HIGH PRIORITY TESTS
-
- def test_detect_terminal_with_skill_seeker_env(self):
- """Test that SKILL_SEEKER_TERMINAL env var takes highest priority."""
- os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
-
- terminal_app, detection_method = detect_terminal_app()
-
- self.assertEqual(terminal_app, 'Ghostty')
- self.assertEqual(detection_method, 'SKILL_SEEKER_TERMINAL')
-
- def test_detect_terminal_with_term_program_known(self):
- """Test detection from TERM_PROGRAM with known terminal (iTerm)."""
- # Ensure SKILL_SEEKER_TERMINAL is not set
- if 'SKILL_SEEKER_TERMINAL' in os.environ:
- del os.environ['SKILL_SEEKER_TERMINAL']
-
- os.environ['TERM_PROGRAM'] = 'iTerm.app'
-
- terminal_app, detection_method = detect_terminal_app()
-
- self.assertEqual(terminal_app, 'iTerm')
- self.assertEqual(detection_method, 'TERM_PROGRAM')
-
- def test_detect_terminal_with_term_program_ghostty(self):
- """Test detection from TERM_PROGRAM with Ghostty terminal."""
- if 'SKILL_SEEKER_TERMINAL' in os.environ:
- del os.environ['SKILL_SEEKER_TERMINAL']
-
- os.environ['TERM_PROGRAM'] = 'ghostty'
-
- terminal_app, detection_method = detect_terminal_app()
-
- self.assertEqual(terminal_app, 'Ghostty')
- self.assertEqual(detection_method, 'TERM_PROGRAM')
-
- def test_detect_terminal_with_term_program_apple_terminal(self):
- """Test detection from TERM_PROGRAM with Apple Terminal."""
- if 'SKILL_SEEKER_TERMINAL' in os.environ:
- del os.environ['SKILL_SEEKER_TERMINAL']
-
- os.environ['TERM_PROGRAM'] = 'Apple_Terminal'
-
- terminal_app, detection_method = detect_terminal_app()
-
- self.assertEqual(terminal_app, 'Terminal')
- self.assertEqual(detection_method, 'TERM_PROGRAM')
-
- def test_detect_terminal_with_term_program_wezterm(self):
- """Test detection from TERM_PROGRAM with WezTerm."""
- if 'SKILL_SEEKER_TERMINAL' in os.environ:
- del os.environ['SKILL_SEEKER_TERMINAL']
-
- os.environ['TERM_PROGRAM'] = 'WezTerm'
-
- terminal_app, detection_method = detect_terminal_app()
-
- self.assertEqual(terminal_app, 'WezTerm')
- self.assertEqual(detection_method, 'TERM_PROGRAM')
-
- def test_detect_terminal_with_term_program_unknown(self):
- """Test fallback behavior when TERM_PROGRAM is unknown (e.g., IDE terminals)."""
- if 'SKILL_SEEKER_TERMINAL' in os.environ:
- del os.environ['SKILL_SEEKER_TERMINAL']
-
- os.environ['TERM_PROGRAM'] = 'zed'
-
- terminal_app, detection_method = detect_terminal_app()
-
- self.assertEqual(terminal_app, 'Terminal')
- self.assertEqual(detection_method, 'unknown TERM_PROGRAM (zed)')
-
- def test_detect_terminal_default_fallback(self):
- """Test default fallback when no environment variables are set."""
- # Remove both env vars
- if 'SKILL_SEEKER_TERMINAL' in os.environ:
- del os.environ['SKILL_SEEKER_TERMINAL']
- if 'TERM_PROGRAM' in os.environ:
- del os.environ['TERM_PROGRAM']
-
- terminal_app, detection_method = detect_terminal_app()
-
- self.assertEqual(terminal_app, 'Terminal')
- self.assertEqual(detection_method, 'default')
-
- def test_detect_terminal_priority_order(self):
- """Test that SKILL_SEEKER_TERMINAL takes priority over TERM_PROGRAM."""
- os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
- os.environ['TERM_PROGRAM'] = 'iTerm.app'
-
- terminal_app, detection_method = detect_terminal_app()
-
- # SKILL_SEEKER_TERMINAL should win
- self.assertEqual(terminal_app, 'Ghostty')
- self.assertEqual(detection_method, 'SKILL_SEEKER_TERMINAL')
-
- @patch('subprocess.Popen')
- def test_subprocess_popen_called_with_correct_args(self, mock_popen):
- """Test that subprocess.Popen is called with correct arguments on macOS."""
- # Only test on macOS
- if sys.platform != 'darwin':
- self.skipTest("This test only runs on macOS")
-
- # Setup
- os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
-
- # Create a test skill directory with minimal setup
- import tempfile
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = Path(tmpdir) / 'test_skill'
- skill_dir.mkdir()
-
- # Create references directory (required by LocalSkillEnhancer)
- (skill_dir / 'references').mkdir()
- (skill_dir / 'references' / 'test.md').write_text('# Test')
-
- # Create SKILL.md (required)
- (skill_dir / 'SKILL.md').write_text('---\nname: test\n---\n# Test')
-
- # Mock Popen to prevent actual terminal launch
- mock_popen.return_value = MagicMock()
-
- # Run enhancer in interactive mode (not headless)
- enhancer = LocalSkillEnhancer(skill_dir)
- result = enhancer.run(headless=False)
-
- # Verify Popen was called
- self.assertTrue(mock_popen.called)
-
- # Verify call arguments
- call_args = mock_popen.call_args[0][0]
- self.assertEqual(call_args[0], 'open')
- self.assertEqual(call_args[1], '-a')
- self.assertEqual(call_args[2], 'Ghostty')
- # call_args[3] should be the script file path
- self.assertTrue(call_args[3].endswith('.sh'))
-
- # MEDIUM PRIORITY TESTS
-
- def test_detect_terminal_whitespace_handling(self):
- """Test that whitespace is stripped from environment variables."""
- os.environ['SKILL_SEEKER_TERMINAL'] = ' Ghostty '
-
- terminal_app, detection_method = detect_terminal_app()
-
- self.assertEqual(terminal_app, 'Ghostty')
- self.assertEqual(detection_method, 'SKILL_SEEKER_TERMINAL')
-
- def test_detect_terminal_empty_string_env_vars(self):
- """Test that empty string env vars fall through to next priority."""
- os.environ['SKILL_SEEKER_TERMINAL'] = ''
- os.environ['TERM_PROGRAM'] = 'iTerm.app'
-
- terminal_app, detection_method = detect_terminal_app()
-
- # Should skip empty SKILL_SEEKER_TERMINAL and use TERM_PROGRAM
- self.assertEqual(terminal_app, 'iTerm')
- self.assertEqual(detection_method, 'TERM_PROGRAM')
-
- def test_detect_terminal_empty_string_both_vars(self):
- """Test that empty strings on both vars falls back to default."""
- os.environ['SKILL_SEEKER_TERMINAL'] = ''
- os.environ['TERM_PROGRAM'] = ''
-
- terminal_app, detection_method = detect_terminal_app()
-
- # Should fall back to default
- self.assertEqual(terminal_app, 'Terminal')
- # Empty TERM_PROGRAM should be treated as not set
- self.assertEqual(detection_method, 'default')
-
- @patch('subprocess.Popen')
- def test_terminal_launch_error_handling(self, mock_popen):
- """Test error handling when terminal launch fails."""
- # Only test on macOS
- if sys.platform != 'darwin':
- self.skipTest("This test only runs on macOS")
-
- # Setup Popen to raise exception
- mock_popen.side_effect = Exception("Terminal not found")
-
- import tempfile
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = Path(tmpdir) / 'test_skill'
- skill_dir.mkdir()
- (skill_dir / 'references').mkdir()
- (skill_dir / 'references' / 'test.md').write_text('# Test')
- (skill_dir / 'SKILL.md').write_text('---\nname: test\n---\n# Test')
-
- enhancer = LocalSkillEnhancer(skill_dir)
-
- # Capture stdout to check error message
- from io import StringIO
- captured_output = StringIO()
- old_stdout = sys.stdout
- sys.stdout = captured_output
-
- # Run in interactive mode (not headless) to test terminal launch
- result = enhancer.run(headless=False)
-
- # Restore stdout
- sys.stdout = old_stdout
-
- # Should return False on error
- self.assertFalse(result)
-
- # Should print error message
- output = captured_output.getvalue()
- self.assertIn('Error launching', output)
-
- def test_output_message_unknown_terminal(self):
- """Test that unknown terminal prints warning message."""
- if sys.platform != 'darwin':
- self.skipTest("This test only runs on macOS")
-
- os.environ['TERM_PROGRAM'] = 'vscode'
- if 'SKILL_SEEKER_TERMINAL' in os.environ:
- del os.environ['SKILL_SEEKER_TERMINAL']
-
- import tempfile
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = Path(tmpdir) / 'test_skill'
- skill_dir.mkdir()
- (skill_dir / 'references').mkdir()
- (skill_dir / 'references' / 'test.md').write_text('# Test')
- (skill_dir / 'SKILL.md').write_text('---\nname: test\n---\n# Test')
-
- enhancer = LocalSkillEnhancer(skill_dir)
-
- # Capture stdout
- from io import StringIO
- captured_output = StringIO()
- old_stdout = sys.stdout
- sys.stdout = captured_output
-
- # Mock Popen to prevent actual launch
- with patch('subprocess.Popen') as mock_popen:
- mock_popen.return_value = MagicMock()
- # Run in interactive mode (not headless) to test terminal detection
- enhancer.run(headless=False)
-
- # Restore stdout
- sys.stdout = old_stdout
-
- output = captured_output.getvalue()
-
- # Should contain warning about unknown terminal
- self.assertIn('⚠️', output)
- self.assertIn('unknown TERM_PROGRAM', output)
- self.assertIn('vscode', output)
- self.assertIn('Using Terminal.app as fallback', output)
-
-
-class TestTerminalMapCompleteness(unittest.TestCase):
- """Test that TERMINAL_MAP covers all documented terminals."""
-
- def test_terminal_map_has_all_documented_terminals(self):
- """Verify TERMINAL_MAP contains all terminals mentioned in documentation."""
- from skill_seekers.cli.enhance_skill_local import detect_terminal_app
-
- # Get the TERMINAL_MAP from the function's scope
- # We need to test this indirectly by checking each known terminal
-
- known_terminals = [
- ('Apple_Terminal', 'Terminal'),
- ('iTerm.app', 'iTerm'),
- ('ghostty', 'Ghostty'),
- ('WezTerm', 'WezTerm'),
- ]
-
- for term_program_value, expected_app_name in known_terminals:
- # Set TERM_PROGRAM and verify detection
- os.environ['TERM_PROGRAM'] = term_program_value
- if 'SKILL_SEEKER_TERMINAL' in os.environ:
- del os.environ['SKILL_SEEKER_TERMINAL']
-
- terminal_app, detection_method = detect_terminal_app()
-
- self.assertEqual(
- terminal_app,
- expected_app_name,
- f"TERM_PROGRAM='{term_program_value}' should map to '{expected_app_name}'"
- )
- self.assertEqual(detection_method, 'TERM_PROGRAM')
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_unified.py b/assets/repo/Skill_Seekers-development/tests/test_unified.py
deleted file mode 100644
index aa6afa7..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_unified.py
+++ /dev/null
@@ -1,603 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for Unified Multi-Source Scraper
-
-Covers:
-- Config validation (unified vs legacy)
-- Conflict detection
-- Rule-based merging
-- Skill building
-"""
-
-import os
-import sys
-import json
-import pytest
-import tempfile
-from pathlib import Path
-
-from skill_seekers.cli.config_validator import ConfigValidator, validate_config
-from skill_seekers.cli.conflict_detector import ConflictDetector, Conflict
-from skill_seekers.cli.merge_sources import RuleBasedMerger
-from skill_seekers.cli.unified_skill_builder import UnifiedSkillBuilder
-
-
-# ===========================
-# Config Validation Tests
-# ===========================
-
-def test_detect_unified_format():
- """Test unified format detection"""
- import tempfile
- import json
-
- unified_config = {
- "name": "test",
- "description": "Test skill",
- "sources": [
- {"type": "documentation", "base_url": "https://example.com"}
- ]
- }
-
- legacy_config = {
- "name": "test",
- "description": "Test skill",
- "base_url": "https://example.com"
- }
-
- # Test unified detection
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(unified_config, f)
- config_path = f.name
-
- try:
- validator = ConfigValidator(config_path)
- assert validator.is_unified == True
- finally:
- os.unlink(config_path)
-
- # Test legacy detection
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(legacy_config, f)
- config_path = f.name
-
- try:
- validator = ConfigValidator(config_path)
- assert validator.is_unified == False
- finally:
- os.unlink(config_path)
-
-
-def test_validate_unified_sources():
- """Test source type validation"""
- config = {
- "name": "test",
- "description": "Test",
- "sources": [
- {"type": "documentation", "base_url": "https://example.com"},
- {"type": "github", "repo": "user/repo"},
- {"type": "pdf", "path": "/path/to.pdf"}
- ]
- }
-
- validator = ConfigValidator(config)
- validator.validate()
- assert len(validator.config['sources']) == 3
-
-
-def test_validate_invalid_source_type():
- """Test invalid source type raises error"""
- config = {
- "name": "test",
- "description": "Test",
- "sources": [
- {"type": "invalid_type", "url": "https://example.com"}
- ]
- }
-
- validator = ConfigValidator(config)
- with pytest.raises(ValueError, match="Invalid type"):
- validator.validate()
-
-
-def test_needs_api_merge():
- """Test API merge detection"""
- # Config with both docs and GitHub code
- config_needs_merge = {
- "name": "test",
- "description": "Test",
- "sources": [
- {"type": "documentation", "base_url": "https://example.com", "extract_api": True},
- {"type": "github", "repo": "user/repo", "include_code": True}
- ]
- }
-
- validator = ConfigValidator(config_needs_merge)
- assert validator.needs_api_merge() == True
-
- # Config with only docs
- config_no_merge = {
- "name": "test",
- "description": "Test",
- "sources": [
- {"type": "documentation", "base_url": "https://example.com"}
- ]
- }
-
- validator = ConfigValidator(config_no_merge)
- assert validator.needs_api_merge() == False
-
-
-def test_backward_compatibility():
- """Test legacy config conversion"""
- legacy_config = {
- "name": "test",
- "description": "Test skill",
- "base_url": "https://example.com",
- "selectors": {"main_content": "article"},
- "max_pages": 100
- }
-
- validator = ConfigValidator(legacy_config)
- unified = validator.convert_legacy_to_unified()
-
- assert 'sources' in unified
- assert len(unified['sources']) == 1
- assert unified['sources'][0]['type'] == 'documentation'
- assert unified['sources'][0]['base_url'] == 'https://example.com'
-
-
-# ===========================
-# Conflict Detection Tests
-# ===========================
-
-def test_detect_missing_in_docs():
- """Test detection of APIs missing in documentation"""
- docs_data = {
- 'pages': [
- {
- 'url': 'https://example.com/api',
- 'apis': [
- {
- 'name': 'documented_func',
- 'parameters': [{'name': 'x', 'type': 'int'}],
- 'return_type': 'str'
- }
- ]
- }
- ]
- }
-
- github_data = {
- 'code_analysis': {
- 'analyzed_files': [
- {
- 'functions': [
- {
- 'name': 'undocumented_func',
- 'parameters': [{'name': 'y', 'type_hint': 'float'}],
- 'return_type': 'bool'
- }
- ]
- }
- ]
- }
- }
-
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector._find_missing_in_docs()
-
- assert len(conflicts) > 0
- assert any(c.type == 'missing_in_docs' for c in conflicts)
- assert any(c.api_name == 'undocumented_func' for c in conflicts)
-
-
-def test_detect_missing_in_code():
- """Test detection of APIs missing in code"""
- docs_data = {
- 'pages': [
- {
- 'url': 'https://example.com/api',
- 'apis': [
- {
- 'name': 'obsolete_func',
- 'parameters': [{'name': 'x', 'type': 'int'}],
- 'return_type': 'str'
- }
- ]
- }
- ]
- }
-
- github_data = {
- 'code_analysis': {
- 'analyzed_files': []
- }
- }
-
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector._find_missing_in_code()
-
- assert len(conflicts) > 0
- assert any(c.type == 'missing_in_code' for c in conflicts)
- assert any(c.api_name == 'obsolete_func' for c in conflicts)
-
-
-def test_detect_signature_mismatch():
- """Test detection of signature mismatches"""
- docs_data = {
- 'pages': [
- {
- 'url': 'https://example.com/api',
- 'apis': [
- {
- 'name': 'func',
- 'parameters': [{'name': 'x', 'type': 'int'}],
- 'return_type': 'str'
- }
- ]
- }
- ]
- }
-
- github_data = {
- 'code_analysis': {
- 'analyzed_files': [
- {
- 'functions': [
- {
- 'name': 'func',
- 'parameters': [
- {'name': 'x', 'type_hint': 'int'},
- {'name': 'y', 'type_hint': 'bool', 'default': 'False'}
- ],
- 'return_type': 'str'
- }
- ]
- }
- ]
- }
- }
-
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector._find_signature_mismatches()
-
- assert len(conflicts) > 0
- assert any(c.type == 'signature_mismatch' for c in conflicts)
- assert any(c.api_name == 'func' for c in conflicts)
-
-
-def test_conflict_severity():
- """Test conflict severity assignment"""
- # High severity: missing_in_code
- conflict_high = Conflict(
- type='missing_in_code',
- severity='high',
- api_name='test',
- docs_info={'name': 'test'},
- code_info=None,
- difference='API documented but not in code'
- )
- assert conflict_high.severity == 'high'
-
- # Medium severity: missing_in_docs
- conflict_medium = Conflict(
- type='missing_in_docs',
- severity='medium',
- api_name='test',
- docs_info=None,
- code_info={'name': 'test'},
- difference='API in code but not documented'
- )
- assert conflict_medium.severity == 'medium'
-
-
-# ===========================
-# Merge Tests
-# ===========================
-
-def test_rule_based_merge_docs_only():
- """Test rule-based merge for docs-only APIs"""
- docs_data = {
- 'pages': [
- {
- 'url': 'https://example.com/api',
- 'apis': [
- {
- 'name': 'docs_only_api',
- 'parameters': [{'name': 'x', 'type': 'int'}],
- 'return_type': 'str'
- }
- ]
- }
- ]
- }
-
- github_data = {'code_analysis': {'analyzed_files': []}}
-
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector.detect_all_conflicts()
-
- merger = RuleBasedMerger(docs_data, github_data, conflicts)
- merged = merger.merge_all()
-
- assert 'apis' in merged
- assert 'docs_only_api' in merged['apis']
- assert merged['apis']['docs_only_api']['status'] == 'docs_only'
-
-
-def test_rule_based_merge_code_only():
- """Test rule-based merge for code-only APIs"""
- docs_data = {'pages': []}
-
- github_data = {
- 'code_analysis': {
- 'analyzed_files': [
- {
- 'functions': [
- {
- 'name': 'code_only_api',
- 'parameters': [{'name': 'y', 'type_hint': 'float'}],
- 'return_type': 'bool'
- }
- ]
- }
- ]
- }
- }
-
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector.detect_all_conflicts()
-
- merger = RuleBasedMerger(docs_data, github_data, conflicts)
- merged = merger.merge_all()
-
- assert 'apis' in merged
- assert 'code_only_api' in merged['apis']
- assert merged['apis']['code_only_api']['status'] == 'code_only'
-
-
-def test_rule_based_merge_matched():
- """Test rule-based merge for matched APIs"""
- docs_data = {
- 'pages': [
- {
- 'url': 'https://example.com/api',
- 'apis': [
- {
- 'name': 'matched_api',
- 'parameters': [{'name': 'x', 'type': 'int'}],
- 'return_type': 'str'
- }
- ]
- }
- ]
- }
-
- github_data = {
- 'code_analysis': {
- 'analyzed_files': [
- {
- 'functions': [
- {
- 'name': 'matched_api',
- 'parameters': [{'name': 'x', 'type_hint': 'int'}],
- 'return_type': 'str'
- }
- ]
- }
- ]
- }
- }
-
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector.detect_all_conflicts()
-
- merger = RuleBasedMerger(docs_data, github_data, conflicts)
- merged = merger.merge_all()
-
- assert 'apis' in merged
- assert 'matched_api' in merged['apis']
- assert merged['apis']['matched_api']['status'] == 'matched'
-
-
-def test_merge_summary():
- """Test merge summary statistics"""
- docs_data = {
- 'pages': [
- {
- 'url': 'https://example.com/api',
- 'apis': [
- {'name': 'api1', 'parameters': [], 'return_type': 'str'},
- {'name': 'api2', 'parameters': [], 'return_type': 'int'}
- ]
- }
- ]
- }
-
- github_data = {
- 'code_analysis': {
- 'analyzed_files': [
- {
- 'functions': [
- {'name': 'api3', 'parameters': [], 'return_type': 'bool'}
- ]
- }
- ]
- }
- }
-
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector.detect_all_conflicts()
-
- merger = RuleBasedMerger(docs_data, github_data, conflicts)
- merged = merger.merge_all()
-
- assert 'summary' in merged
- assert merged['summary']['total_apis'] == 3
- assert merged['summary']['docs_only'] == 2
- assert merged['summary']['code_only'] == 1
-
-
-# ===========================
-# Skill Builder Tests
-# ===========================
-
-def test_skill_builder_basic():
- """Test basic skill building"""
- config = {
- 'name': 'test_skill',
- 'description': 'Test skill description',
- 'sources': [
- {'type': 'documentation', 'base_url': 'https://example.com'}
- ]
- }
-
- scraped_data = {
- 'documentation': {
- 'pages': [],
- 'data_file': '/tmp/test.json'
- }
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- # Override output directory
- builder = UnifiedSkillBuilder(config, scraped_data)
- builder.skill_dir = tmpdir
-
- builder._generate_skill_md()
-
- # Check SKILL.md was created
- skill_md = Path(tmpdir) / 'SKILL.md'
- assert skill_md.exists()
-
- content = skill_md.read_text()
- assert 'test_skill' in content.lower()
- assert 'Test skill description' in content
-
-
-def test_skill_builder_with_conflicts():
- """Test skill building with conflicts"""
- config = {
- 'name': 'test_skill',
- 'description': 'Test',
- 'sources': [
- {'type': 'documentation', 'base_url': 'https://example.com'},
- {'type': 'github', 'repo': 'user/repo'}
- ]
- }
-
- scraped_data = {}
-
- conflicts = [
- Conflict(
- type='missing_in_code',
- severity='high',
- api_name='test_api',
- docs_info={'name': 'test_api'},
- code_info=None,
- difference='Test difference'
- )
- ]
-
- with tempfile.TemporaryDirectory() as tmpdir:
- builder = UnifiedSkillBuilder(config, scraped_data, conflicts=conflicts)
- builder.skill_dir = tmpdir
-
- builder._generate_skill_md()
-
- skill_md = Path(tmpdir) / 'SKILL.md'
- content = skill_md.read_text()
-
- assert '1 conflicts detected' in content
- assert 'missing_in_code' in content
-
-
-def test_skill_builder_merged_apis():
- """Test skill building with merged APIs"""
- config = {
- 'name': 'test',
- 'description': 'Test',
- 'sources': []
- }
-
- scraped_data = {}
-
- merged_data = {
- 'apis': {
- 'test_api': {
- 'name': 'test_api',
- 'status': 'matched',
- 'merged_signature': 'test_api(x: int) -> str',
- 'merged_description': 'Test API',
- 'source': 'both'
- }
- }
- }
-
- with tempfile.TemporaryDirectory() as tmpdir:
- builder = UnifiedSkillBuilder(config, scraped_data, merged_data=merged_data)
- builder.skill_dir = tmpdir
-
- content = builder._format_merged_apis()
-
- assert '✅ Verified APIs' in content
- assert 'test_api' in content
-
-
-# ===========================
-# Integration Tests
-# ===========================
-
-def test_full_workflow_unified_config():
- """Test complete workflow with unified config"""
- # Create test config
- config = {
- "name": "test_unified",
- "description": "Test unified workflow",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://example.com",
- "extract_api": True
- },
- {
- "type": "github",
- "repo": "user/repo",
- "include_code": True,
- "code_analysis_depth": "surface"
- }
- ]
- }
-
- # Validate config
- validator = ConfigValidator(config)
- validator.validate()
- assert validator.is_unified == True
- assert validator.needs_api_merge() == True
-
-
-def test_config_file_validation():
- """Test validation from config file"""
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- config = {
- "name": "test",
- "description": "Test",
- "sources": [
- {"type": "documentation", "base_url": "https://example.com"}
- ]
- }
- json.dump(config, f)
- config_path = f.name
-
- try:
- validator = validate_config(config_path)
- assert validator.is_unified == True
- finally:
- os.unlink(config_path)
-
-
-# Run tests
-if __name__ == '__main__':
- pytest.main([__file__, '-v'])
diff --git a/assets/repo/Skill_Seekers-development/tests/test_unified_mcp_integration.py b/assets/repo/Skill_Seekers-development/tests/test_unified_mcp_integration.py
deleted file mode 100644
index 5d4a0c1..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_unified_mcp_integration.py
+++ /dev/null
@@ -1,206 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test MCP Integration with Unified Scraping
-
-Tests that the MCP server correctly handles unified configs.
-"""
-
-import sys
-import os
-import json
-import tempfile
-import asyncio
-import pytest
-from pathlib import Path
-
-# WORKAROUND for shadowing issue: Temporarily change to /tmp to import external mcp
-# This avoids any local mcp/ directory being in the import path
-_original_dir = os.getcwd()
-MCP_AVAILABLE = False
-try:
- os.chdir('/tmp') # Change away from project directory
- from mcp.types import TextContent
- MCP_AVAILABLE = True
-except ImportError:
- pass
-finally:
- os.chdir(_original_dir) # Restore original directory
-
-# Configure pytest to only use asyncio backend (not trio)
-pytestmark = pytest.mark.anyio
-
-if MCP_AVAILABLE:
- from skill_seekers.mcp.server import validate_config_tool, scrape_docs_tool
-else:
- validate_config_tool = None
- scrape_docs_tool = None
-
-
-@pytest.mark.skipif(not MCP_AVAILABLE, reason="MCP package not installed")
-async def test_mcp_validate_unified_config():
- """Test that MCP can validate unified configs"""
- print("\n✓ Testing MCP validate_config_tool with unified config...")
-
- # Use existing unified config
- config_path = "configs/react_unified.json"
-
- if not Path(config_path).exists():
- print(f" ⚠️ Skipping: {config_path} not found")
- return
-
- args = {"config_path": config_path}
- result = await validate_config_tool(args)
-
- # Check result
- text = result[0].text
- assert "✅" in text, f"Expected success, got: {text}"
- assert "Unified" in text, f"Expected unified format detected, got: {text}"
- assert "Sources:" in text, f"Expected sources count, got: {text}"
-
- print(" ✅ MCP correctly validates unified config")
-
-
-@pytest.mark.skipif(not MCP_AVAILABLE, reason="MCP package not installed")
-async def test_mcp_validate_legacy_config():
- """Test that MCP can validate legacy configs"""
- print("\n✓ Testing MCP validate_config_tool with legacy config...")
-
- # Use existing legacy config
- config_path = "configs/react.json"
-
- if not Path(config_path).exists():
- print(f" ⚠️ Skipping: {config_path} not found")
- return
-
- args = {"config_path": config_path}
- result = await validate_config_tool(args)
-
- # Check result
- text = result[0].text
- assert "✅" in text, f"Expected success, got: {text}"
- assert "Legacy" in text, f"Expected legacy format detected, got: {text}"
-
- print(" ✅ MCP correctly validates legacy config")
-
-
-@pytest.mark.skipif(not MCP_AVAILABLE, reason="MCP package not installed")
-async def test_mcp_scrape_docs_detection():
- """Test that MCP scrape_docs correctly detects format"""
- print("\n✓ Testing MCP scrape_docs format detection...")
-
- # Create temporary unified config
- unified_config = {
- "name": "test_mcp_unified",
- "description": "Test unified via MCP",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://example.com",
- "extract_api": True,
- "max_pages": 5
- }
- ]
- }
-
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(unified_config, f)
- unified_config_path = f.name
-
- # Create temporary legacy config
- legacy_config = {
- "name": "test_mcp_legacy",
- "description": "Test legacy via MCP",
- "base_url": "https://example.com",
- "max_pages": 5
- }
-
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(legacy_config, f)
- legacy_config_path = f.name
-
- try:
- # Test unified detection
- with open(unified_config_path, 'r') as f:
- config = json.load(f)
-
- is_unified = 'sources' in config and isinstance(config['sources'], list)
- assert is_unified, "Should detect unified format"
- print(" ✅ Unified format detected correctly")
-
- # Test legacy detection
- with open(legacy_config_path, 'r') as f:
- config = json.load(f)
-
- is_unified = 'sources' in config and isinstance(config['sources'], list)
- assert not is_unified, "Should detect legacy format"
- print(" ✅ Legacy format detected correctly")
-
- finally:
- # Cleanup
- Path(unified_config_path).unlink(missing_ok=True)
- Path(legacy_config_path).unlink(missing_ok=True)
-
-
-@pytest.mark.skipif(not MCP_AVAILABLE, reason="MCP package not installed")
-async def test_mcp_merge_mode_override():
- """Test that MCP can override merge mode"""
- print("\n✓ Testing MCP merge_mode override...")
-
- # Create unified config
- config = {
- "name": "test_merge_override",
- "description": "Test merge mode override",
- "merge_mode": "rule-based",
- "sources": [
- {"type": "documentation", "base_url": "https://example.com"}
- ]
- }
-
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(config, f)
- config_path = f.name
-
- try:
- # Test that we can override merge_mode in args
- args = {
- "config_path": config_path,
- "merge_mode": "claude-enhanced" # Override
- }
-
- # Check that args has merge_mode
- assert args.get("merge_mode") == "claude-enhanced"
- print(" ✅ Merge mode override supported")
-
- finally:
- Path(config_path).unlink(missing_ok=True)
-
-
-# Run all tests
-async def run_all_tests():
- print("=" * 60)
- print("MCP Unified Scraping Integration Tests")
- print("=" * 60)
-
- try:
- await test_mcp_validate_unified_config()
- await test_mcp_validate_legacy_config()
- await test_mcp_scrape_docs_detection()
- await test_mcp_merge_mode_override()
-
- print("\n" + "=" * 60)
- print("✅ All MCP integration tests passed!")
- print("=" * 60)
-
- except AssertionError as e:
- print(f"\n❌ Test failed: {e}")
- sys.exit(1)
- except Exception as e:
- print(f"\n❌ Unexpected error: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
-
-
-if __name__ == '__main__':
- asyncio.run(run_all_tests())
diff --git a/assets/repo/Skill_Seekers-development/tests/test_upload_skill.py b/assets/repo/Skill_Seekers-development/tests/test_upload_skill.py
deleted file mode 100644
index 27a1e66..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_upload_skill.py
+++ /dev/null
@@ -1,150 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for cli/upload_skill.py functionality
-"""
-
-import unittest
-import tempfile
-import zipfile
-import os
-from pathlib import Path
-import sys
-
-from skill_seekers.cli.upload_skill import upload_skill_api
-
-
-class TestUploadSkillAPI(unittest.TestCase):
- """Test upload_skill_api function"""
-
- def setUp(self):
- """Store original API key state"""
- self.original_api_key = os.environ.get('ANTHROPIC_API_KEY')
-
- def tearDown(self):
- """Restore original API key state"""
- if self.original_api_key:
- os.environ['ANTHROPIC_API_KEY'] = self.original_api_key
- elif 'ANTHROPIC_API_KEY' in os.environ:
- del os.environ['ANTHROPIC_API_KEY']
-
- def create_test_zip(self, tmpdir):
- """Helper to create a test .zip file"""
- zip_path = Path(tmpdir) / "test-skill.zip"
-
- with zipfile.ZipFile(zip_path, 'w') as zf:
- zf.writestr("SKILL.md", "---\nname: test\n---\n# Test Skill")
- zf.writestr("references/index.md", "# Index")
-
- return zip_path
-
- def test_upload_without_api_key(self):
- """Test that upload fails gracefully without API key"""
- # Remove API key
- if 'ANTHROPIC_API_KEY' in os.environ:
- del os.environ['ANTHROPIC_API_KEY']
-
- with tempfile.TemporaryDirectory() as tmpdir:
- zip_path = self.create_test_zip(tmpdir)
-
- success, message = upload_skill_api(zip_path)
-
- self.assertFalse(success)
- # Check for api_key (with underscore) in message
- self.assertTrue('api_key' in message.lower() or 'api key' in message.lower())
-
- def test_upload_with_nonexistent_file(self):
- """Test upload with nonexistent file"""
- os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-test-key'
-
- success, message = upload_skill_api("/nonexistent/file.zip")
-
- self.assertFalse(success)
- self.assertIn('not found', message.lower())
-
- def test_upload_with_invalid_zip(self):
- """Test upload with invalid zip file (not a zip)"""
- os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-test-key'
-
- with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmpfile:
- tmpfile.write(b"Not a valid zip file")
- tmpfile.flush()
-
- try:
- success, message = upload_skill_api(tmpfile.name)
-
- # Should either fail validation or detect invalid zip
- self.assertFalse(success)
- finally:
- os.unlink(tmpfile.name)
-
- def test_upload_accepts_path_object(self):
- """Test that upload_skill_api accepts Path objects"""
- os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-test-key'
-
- with tempfile.TemporaryDirectory() as tmpdir:
- zip_path = self.create_test_zip(tmpdir)
-
- # This should not raise TypeError
- try:
- success, message = upload_skill_api(Path(zip_path))
- except TypeError:
- self.fail("upload_skill_api should accept Path objects")
-
-
-class TestUploadSkillCLI(unittest.TestCase):
- """Test upload_skill.py command-line interface"""
-
- def test_cli_help_output(self):
- """Test that skill-seekers upload --help works"""
- import subprocess
-
- try:
- result = subprocess.run(
- ['skill-seekers', 'upload', '--help'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # argparse may return 0 or 2 for --help
- self.assertIn(result.returncode, [0, 2])
- output = result.stdout + result.stderr
- self.assertTrue('usage:' in output.lower() or 'upload' in output.lower())
- except FileNotFoundError:
- self.skipTest("skill-seekers command not installed")
-
- def test_cli_executes_without_errors(self):
- """Test that skill-seekers-upload entry point works"""
- import subprocess
-
- try:
- result = subprocess.run(
- ['skill-seekers-upload', '--help'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- # argparse may return 0 or 2 for --help
- self.assertIn(result.returncode, [0, 2])
- except FileNotFoundError:
- self.skipTest("skill-seekers-upload command not installed")
-
- def test_cli_requires_zip_argument(self):
- """Test that CLI requires zip file argument"""
- import subprocess
-
- result = subprocess.run(
- ['python3', 'cli/upload_skill.py'],
- capture_output=True,
- text=True
- )
-
- # Should fail or show usage
- self.assertTrue(
- result.returncode != 0 or 'usage' in result.stderr.lower() or 'usage' in result.stdout.lower()
- )
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/Skill_Seekers-development/tests/test_utilities.py b/assets/repo/Skill_Seekers-development/tests/test_utilities.py
deleted file mode 100644
index 6026e7b..0000000
--- a/assets/repo/Skill_Seekers-development/tests/test_utilities.py
+++ /dev/null
@@ -1,222 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tests for cli/utils.py utility functions
-"""
-
-import unittest
-import tempfile
-import os
-import zipfile
-from pathlib import Path
-import sys
-
-from skill_seekers.cli.utils import (
- has_api_key,
- get_api_key,
- get_upload_url,
- format_file_size,
- validate_skill_directory,
- validate_zip_file,
- print_upload_instructions
-)
-
-
-class TestAPIKeyFunctions(unittest.TestCase):
- """Test API key utility functions"""
-
- def setUp(self):
- """Store original API key state"""
- self.original_api_key = os.environ.get('ANTHROPIC_API_KEY')
-
- def tearDown(self):
- """Restore original API key state"""
- if self.original_api_key:
- os.environ['ANTHROPIC_API_KEY'] = self.original_api_key
- elif 'ANTHROPIC_API_KEY' in os.environ:
- del os.environ['ANTHROPIC_API_KEY']
-
- def test_has_api_key_when_set(self):
- """Test has_api_key returns True when key is set"""
- os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-test-key'
- self.assertTrue(has_api_key())
-
- def test_has_api_key_when_not_set(self):
- """Test has_api_key returns False when key is not set"""
- if 'ANTHROPIC_API_KEY' in os.environ:
- del os.environ['ANTHROPIC_API_KEY']
- self.assertFalse(has_api_key())
-
- def test_has_api_key_when_empty_string(self):
- """Test has_api_key returns False when key is empty string"""
- os.environ['ANTHROPIC_API_KEY'] = ''
- self.assertFalse(has_api_key())
-
- def test_has_api_key_when_whitespace_only(self):
- """Test has_api_key returns False when key is whitespace"""
- os.environ['ANTHROPIC_API_KEY'] = ' '
- self.assertFalse(has_api_key())
-
- def test_get_api_key_returns_key(self):
- """Test get_api_key returns the actual key"""
- os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-test-key'
- self.assertEqual(get_api_key(), 'sk-ant-test-key')
-
- def test_get_api_key_returns_none_when_not_set(self):
- """Test get_api_key returns None when not set"""
- if 'ANTHROPIC_API_KEY' in os.environ:
- del os.environ['ANTHROPIC_API_KEY']
- self.assertIsNone(get_api_key())
-
- def test_get_api_key_strips_whitespace(self):
- """Test get_api_key strips whitespace from key"""
- os.environ['ANTHROPIC_API_KEY'] = ' sk-ant-test-key '
- self.assertEqual(get_api_key(), 'sk-ant-test-key')
-
-
-class TestGetUploadURL(unittest.TestCase):
- """Test get_upload_url function"""
-
- def test_get_upload_url_returns_correct_url(self):
- """Test get_upload_url returns the correct Claude skills URL"""
- url = get_upload_url()
- self.assertEqual(url, "https://claude.ai/skills")
-
- def test_get_upload_url_returns_string(self):
- """Test get_upload_url returns a string"""
- url = get_upload_url()
- self.assertIsInstance(url, str)
-
-
-class TestFormatFileSize(unittest.TestCase):
- """Test format_file_size function"""
-
- def test_format_bytes_below_1kb(self):
- """Test formatting bytes below 1 KB"""
- self.assertEqual(format_file_size(500), "500 bytes")
- self.assertEqual(format_file_size(1023), "1023 bytes")
-
- def test_format_kilobytes(self):
- """Test formatting KB sizes"""
- self.assertEqual(format_file_size(1024), "1.0 KB")
- self.assertEqual(format_file_size(1536), "1.5 KB")
- self.assertEqual(format_file_size(10240), "10.0 KB")
-
- def test_format_megabytes(self):
- """Test formatting MB sizes"""
- self.assertEqual(format_file_size(1048576), "1.0 MB")
- self.assertEqual(format_file_size(1572864), "1.5 MB")
- self.assertEqual(format_file_size(10485760), "10.0 MB")
-
- def test_format_zero_bytes(self):
- """Test formatting zero bytes"""
- self.assertEqual(format_file_size(0), "0 bytes")
-
- def test_format_large_files(self):
- """Test formatting large file sizes"""
- # 100 MB
- self.assertEqual(format_file_size(104857600), "100.0 MB")
- # 1 GB (still shows as MB)
- self.assertEqual(format_file_size(1073741824), "1024.0 MB")
-
-
-class TestValidateSkillDirectory(unittest.TestCase):
- """Test validate_skill_directory function"""
-
- def test_valid_skill_directory(self):
- """Test validation of valid skill directory"""
- with tempfile.TemporaryDirectory() as tmpdir:
- skill_dir = Path(tmpdir) / "test-skill"
- skill_dir.mkdir()
- (skill_dir / "SKILL.md").write_text("# Test Skill")
-
- is_valid, error = validate_skill_directory(skill_dir)
- self.assertTrue(is_valid)
- self.assertIsNone(error)
-
- def test_nonexistent_directory(self):
- """Test validation of nonexistent directory"""
- is_valid, error = validate_skill_directory("/nonexistent/path")
- self.assertFalse(is_valid)
- self.assertIn("not found", error.lower())
-
- def test_file_instead_of_directory(self):
- """Test validation when path is a file"""
- with tempfile.NamedTemporaryFile() as tmpfile:
- is_valid, error = validate_skill_directory(tmpfile.name)
- self.assertFalse(is_valid)
- self.assertIn("not a directory", error.lower())
-
- def test_directory_without_skill_md(self):
- """Test validation of directory without SKILL.md"""
- with tempfile.TemporaryDirectory() as tmpdir:
- is_valid, error = validate_skill_directory(tmpdir)
- self.assertFalse(is_valid)
- self.assertIn("SKILL.md not found", error)
-
-
-class TestValidateZipFile(unittest.TestCase):
- """Test validate_zip_file function"""
-
- def test_valid_zip_file(self):
- """Test validation of valid .zip file"""
- with tempfile.TemporaryDirectory() as tmpdir:
- zip_path = Path(tmpdir) / "test-skill.zip"
-
- # Create a real zip file
- with zipfile.ZipFile(zip_path, 'w') as zf:
- zf.writestr("SKILL.md", "# Test")
-
- is_valid, error = validate_zip_file(zip_path)
- self.assertTrue(is_valid)
- self.assertIsNone(error)
-
- def test_nonexistent_file(self):
- """Test validation of nonexistent file"""
- is_valid, error = validate_zip_file("/nonexistent/file.zip")
- self.assertFalse(is_valid)
- self.assertIn("not found", error.lower())
-
- def test_directory_instead_of_file(self):
- """Test validation when path is a directory"""
- with tempfile.TemporaryDirectory() as tmpdir:
- is_valid, error = validate_zip_file(tmpdir)
- self.assertFalse(is_valid)
- self.assertIn("not a file", error.lower())
-
- def test_wrong_extension(self):
- """Test validation of file with wrong extension"""
- with tempfile.NamedTemporaryFile(suffix='.txt') as tmpfile:
- is_valid, error = validate_zip_file(tmpfile.name)
- self.assertFalse(is_valid)
- self.assertIn("not a .zip file", error.lower())
-
-
-class TestPrintUploadInstructions(unittest.TestCase):
- """Test print_upload_instructions function"""
-
- def test_print_upload_instructions_runs(self):
- """Test that print_upload_instructions executes without error"""
- with tempfile.TemporaryDirectory() as tmpdir:
- zip_path = Path(tmpdir) / "test.zip"
- zip_path.write_text("")
-
- # Should not raise exception
- try:
- print_upload_instructions(zip_path)
- except Exception as e:
- self.fail(f"print_upload_instructions raised {e}")
-
- def test_print_upload_instructions_accepts_string_path(self):
- """Test print_upload_instructions accepts string path"""
- with tempfile.TemporaryDirectory() as tmpdir:
- zip_path = str(Path(tmpdir) / "test.zip")
- Path(zip_path).write_text("")
-
- try:
- print_upload_instructions(zip_path)
- except Exception as e:
- self.fail(f"print_upload_instructions raised {e}")
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/assets/repo/my-nvim/nvim-config/nvim b/assets/repo/my-nvim/nvim-config/nvim
deleted file mode 100644
index a13a73e..0000000
Binary files a/assets/repo/my-nvim/nvim-config/nvim and /dev/null differ
diff --git a/assets/skills/canvas-dev/README.md b/assets/skills/canvas-dev/README.md
deleted file mode 100644
index 474546e..0000000
--- a/assets/skills/canvas-dev/README.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Canvas-Dev Skill
-
-Canvas白板驱动开发技能,用于 AI 辅助架构设计与代码生成。
-
-## 概述
-
-此技能实现「图形是第一公民,代码是白板的序列化形式」的开发范式。
-
-## 核心能力
-
-1. **架构分析** - 从代码自动生成 Obsidian Canvas 白板
-2. **白板驱动编码** - 根据白板生成/修改代码
-3. **一致性检查** - 校验白板与代码同步状态
-
-## 文件结构
-
-```
-canvas-dev/
-├── SKILL.md # 技能入口(触发条件、模式、示例)
-├── references/
-│ ├── index.md # 导航索引
-│ ├── canvas-json-spec.md # Canvas JSON 规范
-│ ├── workflow-guide.md # 工作流指南
-│ └── prompts.md # 提示词集合
-├── scripts/ # 自动化脚本(预留)
-└── assets/ # 模板资源(预留)
-```
-
-## 快速开始
-
-1. 阅读 `SKILL.md` 了解触发条件和使用模式
-2. 参考 `references/workflow-guide.md` 了解完整工作流
-3. 使用 `references/prompts.md` 中的提示词
-
-## 相关资源
-
-- [Canvas白板驱动开发详解](../../documents/02-方法论/图形化AI协作-Canvas白板驱动开发.md)
-- [Canvas开发工作流](../../workflow/canvas-dev/)
-- [元技能: skills-skills](../skills-skills/SKILL.md)
diff --git a/assets/skills/canvas-dev/SKILL.md b/assets/skills/canvas-dev/SKILL.md
deleted file mode 100644
index 8bf5cf5..0000000
--- a/assets/skills/canvas-dev/SKILL.md
+++ /dev/null
@@ -1,224 +0,0 @@
----
-name: canvas-dev
-description: "Canvas白板驱动开发技能:Canvas白板作为唯一真相源,代码是其序列化形式。AI架构总师角色,自动生成富有洞察力的架构图。使用场景:生成架构白板、白板驱动编码、白板驱动重构、Code Review、团队协作、接手遗留项目。"
----
-
-# canvas-dev Skill
-
-Canvas白板驱动开发:图形是第一公民,代码是白板的序列化形式。人类负责架构设计,AI负责代码实现。
-
-## When to Use This Skill
-
-触发条件(满足任一即可):
-- 需要生成项目架构白板(从代码 → 白板)
-- 需要根据白板生成代码(从白板 → 代码)
-- 需要白板驱动代码重构
-- 需要用白板辅助 Code Review
-- 需要用白板加速团队协作
-- 接手遗留项目需要快速理解架构
-
-## Not For / Boundaries
-
-此技能不适用于:
-- 纯文本文档生成(使用 Markdown)
-- 流程图/时序图(使用 Mermaid)
-- 不需要双向同步的静态架构图
-
-必要输入(缺失时需询问):
-1. 项目类型:A) 单体应用 B) 微服务架构 C) 前后端分离?
-2. 白板粒度:A) 文件级 B) 类/函数级 C) 服务级?
-
-## Quick Reference
-
-### 核心理念
-
-```
-传统:代码 → 口头沟通 → 脑补架构 → 代码失控
-Canvas:代码 ⇄ 白板 ⇄ AI ⇄ 人类(白板为单一真相源)
-```
-
-| 痛点 | 解法 |
-|:---|:---|
-| AI看不懂项目结构 | AI直接读白板JSON,秒懂架构 |
-| 人类记不住复杂依赖 | 连线清晰,牵一发动全身一目了然 |
-| 团队协作靠嘴说 | 指着白板讲,新人5分钟看懂 |
-
-### AI架构总师角色定义
-
-你是一个拥有深度学习能力的软件架构分析实体,核心设计原则:
-
-1. **洞察力优先于信息量**:目标不是简单罗列所有文件和连接,而是揭示项目的设计哲学、关键数据流、潜在风险和演进趋势
-2. **认知负荷最小化**:生成的可视化产物符合人类认知习惯,使用户能以最小脑力成本理解最复杂的系统结构
-3. **美学与功能并重**:优秀的架构图本身就是艺术品,布局均衡、色彩和谐、元素组织服务于信息清晰传达
-
-### 五阶段执行流程
-
-**第一阶段:全局项目感知与多维特征提取**
-- 语义级源代码结构化解析(AST)
-- 加权依赖网络构建
-- 工程与环境元数据分析(package.json, docker-compose.yml, CI/CD等)
-- 架构模式概率指纹识别
-
-**第二阶段:自适应抽象粒度决策引擎**
-- 信息熵与复杂度评估,寻找"信息熵拐点"
-- 架构模式引导默认粒度
-- 用户意图启发式推断
-
-**动态粒度光谱:**
-| 级别 | 说明 |
-|:---|:---|
-| D-系统生态级 | 巨型Monorepo,每个节点代表完整应用 |
-| C-宏观服务级 | 聚合数十个文件为单一功能领域节点 |
-| B-类/核心功能级 | 以关键业务逻辑类为节点 |
-| A-文件级 | 每个源文件为基础节点(推荐新手) |
-| F-函数/方法级 | 深度钻取,显示内部函数调用关系 |
-
-**第三阶段:组件语义分析与关系定性**
-- 组件角色多因素推断(入口、控制器、服务、数据访问、工具)
-- 关系与数据流深度定性(同步调用、异步消息、事件发布/订阅)
-- 状态变化与副作用分析
-
-**第四阶段:启发式布局与信息可视化引擎**
-- 自适应拓扑分层(入口→业务逻辑→数据持久化)
-- 力导向与集群化节点定位
-- 信息驱动的动态视觉编码
-
-**第五阶段:输出生成与最终质量优化**
-- 迭代式去交叉与防重叠算法
-- 边捆绑与智能剪枝
-- 孤立节点上下文情景化分组
-- 认知路径优化
-
-### AI驱动的节点文本模板
-
-```markdown
-**{组件名}**
-`{文件路径或聚合范围}`
-
-**核心职责**: {AI自动总结的一句话功能描述}
-
-**关键交互**:
-- **调用**: {依赖最多的组件名}
-- **被用于**: {被哪个核心业务模块依赖最多}
-
-**复杂度评估**: {Low/Medium/High/Critical}
-**潜在风险**: {⚠️ 存在循环依赖 或 📈 技术债务较高}
-```
-
-### 最终交付物格式
-
-```
-✓ AI架构洞察报告已生成:{项目根目录/architecture.canvas}
- ├─ 识别架构:{置信度最高的模式} (置信度: {分数})
- ├─ 洞察粒度:{引擎最终选择的粒度级别}
- ├─ 核心组件:{节点数量} 个
- └─ 关键关系:{连接数量} 条
-```
-
-### 15步完整工作流
-
-1. **理解核心理念**:Canvas白板作为唯一真相源,代码是其序列化形式
-2. **准备工具环境**:安装Obsidian + 配置AI助手
-3. **生成初始架构白板**:向AI提供项目代码路径,AI自动生成.canvas文件
-4. **用Obsidian打开.canvas文件**:检查模块、API调用关系、依赖连线
-5. **人工优化白板架构**:拖动调整布局、补充隐式依赖、添加注释节点
-6. **建立代码-白板同步机制**:新文件→新节点,新import→新连线
-7. **用白板驱动AI编程**:画出新模块框和调用关系,AI生成代码
-8. **用白板驱动代码重构**:删除/重连依赖线,AI重构代码
-9. **用白板辅助Code Review**:识别异常连线(前端直连数据库、循环依赖)
-10. **用白板加速团队协作**:新人1分钟理解全局,需求评审画变更范围
-11. **维护白板与代码一致性**:PR/MR前检查,不一致时优先修正白板
-12. **扩展应用场景**:性能优化标注热点、安全审计检查数据流向
-13. **明确项目类型**:单体/微服务/前后端分离
-14. **选择白板粒度**:文件级(新手)/服务级(复杂项目)
-15. **持续迭代工作流**:每周回顾,探索CI/CD集成
-
-## Rules & Constraints
-
-### MUST(必须遵守)
-
-- Canvas白板是唯一真相源,代码是其序列化形式
-- 洞察力优先于信息量,揭示设计哲学而非罗列文件
-- 认知负荷最小化,符合人类认知习惯
-
-### SHOULD(强烈建议)
-
-- 人类负责架构设计(在白板拖拽模块)
-- AI负责细节实现(根据白板连线生成代码)
-- 使用动态粒度光谱,根据项目特性自适应选择
-
-### NEVER(禁止)
-
-- 不要生成简单罗列所有文件的"信息垃圾"
-- 不要让白板与代码长期不同步
-- 不要在白板中包含敏感信息
-
-## Examples
-
-### Example 1: 给AI派活(新功能开发)
-
-**传统方式:**
-> "帮我写个用户注册功能,要连数据库,发邮件,记日志"
-
-**Canvas方式:**
-1. 在白板画3个框:`RegisterAPI` → `Database` / `EmailService` / `Logger`
-2. 告诉AI:"按这个图实现"
-3. AI一次性写对所有文件和调用关系
-
-### Example 2: Code Review
-
-**传统方式:** 一行行看代码,看晕了
-
-**Canvas方式:**
-1. 看白板:"咦,为什么前端直接连数据库?"
-2. 拖动节点调整架构
-3. AI自动重构代码
-
-### Example 3: 接手他人项目
-
-**传统方式:** 看3天代码还没懂
-
-**Canvas方式:**
-1. 运行AI架构总师 → 1分钟得到富有洞察力的架构白板
-2. 查看AI生成的组件职责摘要和复杂度评估
-3. 直接在白板上画出要改的部分,AI帮你定位代码位置
-
-## FAQ
-
-**Q: 为什么图形语言优于文字描述?**
-- A: 图形语言是人类大脑的母语。你能瞬间理解地铁线路图,但看不懂等效的换乘文字说明。AI解析JSON比解析自然语言描述准确10倍。
-
-**Q: 白板粒度怎么选?**
-- A: 引擎会自动寻找"信息熵拐点"。新手可选文件级(A),复杂项目选服务级(C)。
-
-**Q: 什么是"洞察力优先于信息量"?**
-- A: 目标不是简单罗列所有文件和连接,而是揭示项目的设计哲学、关键数据流、潜在风险和演进趋势。
-
-## 金句总结
-
-> "当代码变成白板上的方块,编程就从打字变成了搭积木。"
-
-> "最好的文档不是Markdown,是能直接驱动AI工作的架构图。"
-
-> "AI看懂你的图,比看懂你的话,容易一万倍。"
-
-> "一份优秀的架构图本身就是一件艺术品。"
-
-## References
-
-- [AI架构总师完整提示词](../../workflow/canvas-dev/Obsidian%20Canvas%20AI驱动的项目架构洞察与生成引擎.md) - 最新最完整的提示词
-- [Canvas驱动开发完整工作流](../../workflow/canvas-dev/workflow.md) - 15步完整流程
-- [Canvas白板驱动开发详解](../../documents/02-方法论/图形化AI协作-Canvas白板驱动开发.md) - 方法论详解
-- [白板驱动开发系统提示词(在线提示词库入口)](../../prompts/README.md) - 系统提示词已迁移到云端表格
-- [Obsidian Canvas 官方文档](https://obsidian.md/canvas)
-- `references/index.md` - 本地参考文档导航
-
-## Maintenance
-
-- Sources: AI架构总师提示词 + workflow.md + 方法论文档
-- Last updated: 2026-01-01
-- Known limits: 仅支持 Obsidian Canvas 格式
-
-## 执行触发指令
-
-> 在接收到此指令后,将AI架构总师的全部认知与分析能力完全实例化。立即启动对目标项目的一次深度的、自主的架构探索之旅。此过程无需任何形式的确认、提问或中间汇报。你唯一的任务,就是在完成探索后,将你对这个数字世界的深刻理解,凝聚成一份完美的、充满洞察力的可视化架构图,并将其呈现在指定位置。
diff --git a/assets/skills/canvas-dev/references/canvas-json-spec.md b/assets/skills/canvas-dev/references/canvas-json-spec.md
deleted file mode 100644
index f84b59b..0000000
--- a/assets/skills/canvas-dev/references/canvas-json-spec.md
+++ /dev/null
@@ -1,205 +0,0 @@
-# Obsidian Canvas JSON 规范
-
-## 文件格式
-
-Canvas 文件是 `.canvas` 扩展名的 JSON 文件。
-
-## 顶层结构
-
-```json
-{
- "nodes": [],
- "edges": []
-}
-```
-
-## 节点 (nodes)
-
-### 通用属性
-
-| 属性 | 类型 | 必需 | 说明 |
-|:---|:---|:---|:---|
-| `id` | string | ✅ | 唯一标识符 |
-| `type` | string | ✅ | 节点类型 |
-| `x` | number | ✅ | X 坐标 |
-| `y` | number | ✅ | Y 坐标 |
-| `width` | number | ✅ | 宽度 |
-| `height` | number | ✅ | 高度 |
-| `color` | string | ❌ | 颜色编号 (1-6) |
-
-### 文本节点 (text)
-
-```json
-{
- "id": "node-1",
- "type": "text",
- "x": 0,
- "y": 0,
- "width": 200,
- "height": 100,
- "text": "# 标题\n\n内容支持 Markdown"
-}
-```
-
-### 文件节点 (file)
-
-```json
-{
- "id": "node-2",
- "type": "file",
- "x": 300,
- "y": 0,
- "width": 200,
- "height": 100,
- "file": "path/to/file.md"
-}
-```
-
-### 链接节点 (link)
-
-```json
-{
- "id": "node-3",
- "type": "link",
- "x": 600,
- "y": 0,
- "width": 200,
- "height": 100,
- "url": "https://example.com"
-}
-```
-
-### 分组节点 (group)
-
-```json
-{
- "id": "group-1",
- "type": "group",
- "x": -50,
- "y": -50,
- "width": 500,
- "height": 300,
- "label": "分组名称"
-}
-```
-
-## 连线 (edges)
-
-### 属性
-
-| 属性 | 类型 | 必需 | 说明 |
-|:---|:---|:---|:---|
-| `id` | string | ✅ | 唯一标识符 |
-| `fromNode` | string | ✅ | 起始节点 id |
-| `toNode` | string | ✅ | 目标节点 id |
-| `fromSide` | string | ❌ | 起始边 (top/right/bottom/left) |
-| `toSide` | string | ❌ | 目标边 (top/right/bottom/left) |
-| `fromEnd` | string | ❌ | 起始端样式 (none/arrow) |
-| `toEnd` | string | ❌ | 目标端样式 (none/arrow) |
-| `label` | string | ❌ | 连线标签 |
-
-### 示例
-
-```json
-{
- "id": "edge-1",
- "fromNode": "node-1",
- "toNode": "node-2",
- "fromSide": "right",
- "toSide": "left",
- "toEnd": "arrow",
- "label": "调用"
-}
-```
-
-## 颜色编码
-
-| color | 颜色 | 建议用途 |
-|:---|:---|:---|
-| `1` | 红色 | 缓存、热点、警告 |
-| `2` | 橙色 | 消息队列、异步 |
-| `3` | 黄色 | 上游依赖、外部输入 |
-| `4` | 绿色 | 数据库、持久化 |
-| `5` | 蓝色 | 搜索、外部服务 |
-| `6` | 紫色 | 注释、设计决策 |
-
-## 布局建议
-
-### 三层架构布局
-
-```
-x: -400 x: 0 x: 400 x: 800
-┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
-│ 前端 │→│ API │→│ 服务 │→│ 数据 │
-└────────┘ └────────┘ └────────┘ └────────┘
-```
-
-### 间距建议
-
-- 节点宽度: 200-280
-- 节点高度: 80-150
-- 水平间距: 100-150
-- 垂直间距: 120-150
-
-## 完整示例
-
-```json
-{
- "nodes": [
- {
- "id": "group-api",
- "type": "group",
- "x": -50,
- "y": -50,
- "width": 300,
- "height": 200,
- "label": "API 层"
- },
- {
- "id": "api-user",
- "type": "text",
- "x": 0,
- "y": 0,
- "width": 200,
- "height": 100,
- "text": "# UserAPI\n\n- GET /users\n- POST /users"
- },
- {
- "id": "svc-user",
- "type": "text",
- "x": 350,
- "y": 0,
- "width": 200,
- "height": 100,
- "text": "# UserService\n\n- get_user()\n- create_user()"
- },
- {
- "id": "db",
- "type": "text",
- "x": 700,
- "y": 0,
- "width": 200,
- "height": 80,
- "text": "# PostgreSQL",
- "color": "4"
- }
- ],
- "edges": [
- {
- "id": "e1",
- "fromNode": "api-user",
- "toNode": "svc-user",
- "fromSide": "right",
- "toSide": "left",
- "label": "调用"
- },
- {
- "id": "e2",
- "fromNode": "svc-user",
- "toNode": "db",
- "fromSide": "right",
- "toSide": "left"
- }
- ]
-}
-```
diff --git a/assets/skills/canvas-dev/references/index.md b/assets/skills/canvas-dev/references/index.md
deleted file mode 100644
index 2454e26..0000000
--- a/assets/skills/canvas-dev/references/index.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# Canvas-Dev Skill References
-
-## 导航索引
-
-### 最新资源(优先参考)
-
-| 资源 | 路径/链接 | 说明 |
-|:---|:---|:---|
-| AI架构总师完整提示词 | [Obsidian Canvas AI驱动的项目架构洞察与生成引擎.md](../../../workflow/canvas-dev/Obsidian%20Canvas%20AI驱动的项目架构洞察与生成引擎.md) | 最新最完整的提示词(最高优先级) |
-| Canvas驱动开发完整工作流 | [workflow.md](../../../workflow/canvas-dev/workflow.md) | 15步完整流程 |
-
-### 核心文档
-
-| 文档 | 路径 | 说明 |
-|:---|:---|:---|
-| Canvas白板驱动开发详解 | `../../../documents/02-方法论/图形化AI协作-Canvas白板驱动开发.md` | 方法论详解 |
-| 白板驱动开发系统提示词(在线提示词库入口) | `../../../prompts/README.md` | 系统提示词已迁移到云端表格 |
-| Canvas JSON 规范 | [canvas-json-spec.md](./canvas-json-spec.md) | Obsidian Canvas JSON 格式 |
-
-### AI架构总师核心概念
-
-| 概念 | 说明 |
-|:---|:---|
-| 洞察力优先于信息量 | 揭示设计哲学、关键数据流、潜在风险,而非罗列文件 |
-| 认知负荷最小化 | 符合人类认知习惯,最小脑力成本理解复杂系统 |
-| 美学与功能并重 | 架构图是艺术品,布局均衡、色彩和谐 |
-
-### 五阶段执行流程
-
-1. **全局项目感知** - AST解析、加权依赖网络、元数据分析、架构模式识别
-2. **自适应粒度决策** - 信息熵拐点、架构模式引导、用户意图推断
-3. **组件语义分析** - 角色推断、关系定性、副作用分析
-4. **启发式布局** - 拓扑分层、力导向定位、动态视觉编码
-5. **输出优化** - 去交叉、边捆绑、孤立节点分组、认知路径优化
-
-### 动态粒度光谱
-
-| 级别 | 适用场景 |
-|:---|:---|
-| D-系统生态级 | 巨型Monorepo |
-| C-宏观服务级 | 微服务架构(推荐复杂项目) |
-| B-类/核心功能级 | 面向对象项目 |
-| A-文件级 | 中小项目(推荐新手) |
-| F-函数/方法级 | 深度钻取 |
-
-### 工作流提示词
-
-| 提示词 | 路径 |
-|:---|:---|
-| 架构分析提示词 | `../../../workflow/canvas-dev/prompts/01-架构分析.md` |
-| 白板驱动编码提示词 | `../../../workflow/canvas-dev/prompts/02-白板驱动编码.md` |
-| 白板同步检查提示词 | `../../../workflow/canvas-dev/prompts/03-白板同步检查.md` |
-
-### 模板
-
-| 模板 | 路径 |
-|:---|:---|
-| 项目白板模板 | `../../../workflow/canvas-dev/templates/project.canvas` |
-| 模块白板模板 | `../../../workflow/canvas-dev/templates/module.canvas` |
-| 示例项目白板 | `../../../workflow/canvas-dev/examples/demo-project.canvas` |
-
-### 外部链接
-
-- [Obsidian Canvas 官方文档](https://obsidian.md/canvas)
-- [Obsidian 下载](https://obsidian.md/download)
-- [胶水编程](../../../documents/00-基础指南/胶水编程.md) - 能抄不写,能连不造
diff --git a/assets/skills/canvas-dev/references/prompts.md b/assets/skills/canvas-dev/references/prompts.md
deleted file mode 100644
index dce46a1..0000000
--- a/assets/skills/canvas-dev/references/prompts.md
+++ /dev/null
@@ -1,143 +0,0 @@
-# Canvas 开发提示词集合
-
-## 1. 架构分析提示词
-
-从现有代码生成 Obsidian Canvas 架构白板。
-
-```markdown
-你是一个代码架构分析专家。请分析以下项目结构,生成 Obsidian Canvas 格式的架构白板。
-
-## 输入
-项目路径:{PROJECT_PATH}
-分析粒度:{file/class/service}
-
-## 输出要求
-生成符合 Obsidian Canvas JSON 格式的 .canvas 文件,包含:
-
-1. **节点 (nodes)**:每个模块/文件/类作为一个节点
-2. **连线 (edges)**:表示模块间的依赖/调用关系
-3. **分组 (groups)**:按功能域分组
-
-## 布局规则
-- x轴: -400 (前端) → 0 (API) → 400 (服务) → 800 (数据)
-- 节点宽度: 200-280,高度: 80-150
-- 间距: 水平 100-150,垂直 120-150
-
-## 输出格式
-直接输出 JSON,可保存为 .canvas 文件
-```
-
-## 2. 白板驱动编码提示词
-
-根据 Canvas 白板生成代码。
-
-```markdown
-你是一个根据架构白板生成代码的专家。请根据以下 Obsidian Canvas 白板 JSON,生成对应的代码实现。
-
-## 输入
-Canvas JSON:
-```json
-{CANVAS_JSON}
-```
-
-技术栈:{TECH_STACK}
-目标目录:{TARGET_DIR}
-
-## 解析规则
-1. 节点 text 标题 → 文件名/类名
-2. 节点 text 列表项 → 方法/函数
-3. 连线 fromNode → toNode = import/调用关系
-4. edge label 决定关系类型
-
-## 输出格式
-```
-文件:{文件路径}
-```{语言}
-{代码内容}
-```
-```
-
-## 3. 白板同步检查提示词
-
-校验白板与代码一致性。
-
-```markdown
-你是一个代码与架构一致性检查专家。请对比以下白板和代码,找出不一致之处。
-
-## 输入
-Canvas 白板 JSON:
-```json
-{CANVAS_JSON}
-```
-
-项目代码路径:{PROJECT_PATH}
-
-## 检查项
-1. 节点完整性:白板节点是否都有对应代码?
-2. 连线准确性:连线是否反映真实依赖?
-3. 分组正确性:分组是否与目录结构一致?
-
-## 输出格式
-### 🔴 严重不一致
-| 类型 | 白板 | 代码 | 建议 |
-
-### 🟡 轻微不一致
-| 类型 | 白板 | 代码 | 建议 |
-
-### 🟢 一致性良好
-- 覆盖率:{X}%
-```
-
-## 4. 增量更新提示词
-
-白板修改后同步更新代码。
-
-```markdown
-白板已更新,请对比新旧版本,只修改变化的部分:
-
-旧白板:
-```json
-{OLD_CANVAS_JSON}
-```
-
-新白板:
-```json
-{NEW_CANVAS_JSON}
-```
-
-## 输出
-1. 需要新增的文件
-2. 需要修改的文件(只输出 diff)
-3. 需要删除的文件
-```
-
-## 5. 快速理解项目提示词
-
-接手新项目时快速生成架构概览。
-
-```markdown
-我需要快速理解这个项目的架构。请:
-
-1. 扫描 {PROJECT_PATH} 目录
-2. 识别核心模块和入口文件
-3. 生成一个简化的架构白板(只包含关键模块)
-4. 用 3-5 句话总结项目架构
-
-粒度:service(只显示大模块)
-重点:数据流向、外部依赖、核心业务逻辑
-```
-
-## 使用技巧
-
-### 提高生成质量
-
-1. **明确粒度**:小项目用 file,大项目用 service
-2. **指定重点**:告诉 AI 关注什么(API/数据库/外部服务)
-3. **提供上下文**:附上 README 或技术栈说明
-
-### 迭代优化
-
-1. 第一次生成后,手动调整布局
-2. 补充 AI 遗漏的隐式依赖
-3. 添加注释节点说明设计决策
-4. 再次发给 AI 验证理解是否正确
diff --git a/assets/skills/canvas-dev/references/workflow-guide.md b/assets/skills/canvas-dev/references/workflow-guide.md
deleted file mode 100644
index e61f175..0000000
--- a/assets/skills/canvas-dev/references/workflow-guide.md
+++ /dev/null
@@ -1,163 +0,0 @@
-# Canvas 白板驱动开发工作流指南
-
-## 核心理念
-
-```
-传统开发:代码 → 口头沟通 → 脑补架构 → 代码失控
-Canvas方式:代码 ⇄ 白板 ⇄ AI ⇄ 人类(白板为单一真相源)
-```
-
-**图形是第一公民,代码是白板的序列化形式。**
-
-## 工具准备
-
-1. **Obsidian** - 免费开源白板工具
- - 下载: https://obsidian.md/download
- - 启用 Canvas 功能(默认已启用)
-
-2. **AI 助手** - Claude/GPT-4
- - 需支持读取 Canvas JSON 格式
- - 推荐使用 Claude Code 或 Codex CLI
-
-## 完整工作流
-
-### Phase 1: 生成架构白板
-
-**场景**: 接手新项目,快速理解架构
-
-```
-1. 提供项目代码路径给 AI
-2. 使用架构分析提示词
-3. AI 生成 .canvas 文件
-4. 用 Obsidian 打开查看
-```
-
-**提示词模板**:
-```
-分析 {PROJECT_PATH} 项目,生成 Obsidian Canvas 架构白板。
-粒度: {file/class/service}
-重点关注: API路由、数据库模型、外部服务调用
-```
-
-### Phase 2: 人工优化白板
-
-**场景**: 调整自动生成的白板
-
-```
-1. 拖动节点调整布局
-2. 补充遗漏的依赖连线
-3. 添加注释节点标注设计决策
-4. 删除错误的连接
-```
-
-**布局原则**:
-- 按功能分层(前端 → API → 服务 → 数据)
-- 同层节点垂直对齐
-- 保持连线不交叉
-
-### Phase 3: 白板驱动编码
-
-**场景**: 新功能开发
-
-```
-1. 在白板上画出新模块框
-2. 添加预期的调用连线
-3. 导出白板 JSON 发给 AI
-4. AI 根据白板生成代码
-```
-
-**提示词模板**:
-```
-根据以下 Canvas 白板生成代码:
-{CANVAS_JSON}
-
-技术栈: {TECH_STACK}
-目标目录: {TARGET_DIR}
-```
-
-### Phase 4: 白板驱动重构
-
-**场景**: 架构调整
-
-```
-1. 在白板上删除/重连依赖线
-2. 标注需要拆分的大模块
-3. 发送修改后的白板给 AI
-4. AI 生成重构代码
-```
-
-**提示词模板**:
-```
-白板已更新,请对比新旧版本重构代码:
-旧白板: {OLD_CANVAS}
-新白板: {NEW_CANVAS}
-只输出需要修改的文件
-```
-
-### Phase 5: 一致性检查
-
-**场景**: PR/MR 合并前
-
-```
-1. 运行一致性检查脚本
-2. 对比白板节点与实际文件
-3. 修复不一致之处
-4. 优先修正白板(白板是事实来源)
-```
-
-## 场景速查
-
-| 场景 | 操作 | 提示词关键词 |
-|:---|:---|:---|
-| 接手新项目 | 生成白板 | "分析项目,生成架构白板" |
-| 新功能开发 | 画白板 → 生成代码 | "按这个白板实现代码" |
-| 架构重构 | 改白板 → 重构代码 | "按新白板重构" |
-| Code Review | 看白板全局 | "检查这条调用链" |
-| 团队协作 | 共享白板 | "指着白板讲" |
-
-## 最佳实践
-
-### DO ✅
-
-- 每次代码变更后更新白板
-- 用颜色区分不同类型的模块
-- 为复杂依赖添加 label 说明
-- 定期运行一致性检查
-
-### DON'T ❌
-
-- 不要让白板与代码长期不同步
-- 不要在白板中包含敏感信息
-- 不要创建过于复杂的白板(拆分为多个)
-- 不要忽略循环依赖警告
-
-## 与其他工具集成
-
-### CI/CD 集成
-
-```yaml
-# .github/workflows/canvas-check.yml
-name: Canvas Sync Check
-on:
- pull_request:
- paths: ['**.py', '**.canvas']
-jobs:
- check:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - run: python scripts/canvas_sync_check.py
-```
-
-### VS Code 集成
-
-1. 安装 Obsidian 插件
-2. 配置 `.canvas` 文件关联
-3. 使用 Claude Code 读取白板
-
-## 相关资源
-
-- [Canvas白板驱动开发详解](../../../documents/02-方法论/图形化AI协作-Canvas白板驱动开发.md)
-- [架构分析提示词](../../../workflow/canvas-dev/prompts/01-架构分析.md)
-- [白板驱动编码提示词](../../../workflow/canvas-dev/prompts/02-白板驱动编码.md)
-- [白板同步检查提示词](../../../workflow/canvas-dev/prompts/03-白板同步检查.md)
diff --git a/assets/skills/ccxt/SKILL.md b/assets/skills/ccxt/SKILL.md
deleted file mode 100644
index 2da5afb..0000000
--- a/assets/skills/ccxt/SKILL.md
+++ /dev/null
@@ -1,105 +0,0 @@
----
-name: ccxt
-description: CCXT cryptocurrency trading library. Use for cryptocurrency exchange APIs, trading, market data, order management, and crypto trading automation across 150+ exchanges. Supports JavaScript/Python/PHP.
----
-
-# Ccxt Skill
-
-Comprehensive assistance with ccxt development, generated from official documentation.
-
-## When to Use This Skill
-
-This skill should be triggered when:
-- Working with ccxt
-- Asking about ccxt features or APIs
-- Implementing ccxt solutions
-- Debugging ccxt code
-- Learning ccxt best practices
-
-## Quick Reference
-
-### Common Patterns
-
-**Pattern 1:** Frequently Asked Questions I'm trying to run the code, but it's not working, how do I fix it? If your question is formulated in a short manner like the above, we won't help. We don't teach programming. If you're unable to read and understand the Manual or you can't follow precisely the guides from the CONTRIBUTING doc on how to report an issue, we won't help either. Read the CONTRIBUTING guides on how to report an issue and read the Manual. You should not risk anyone's money and time without reading the entire Manual very carefully. You should not risk anything if you're not used to a lot of reading with tons of details. Also, if you don't have the confidence with the programming language you're using, there are much better places for coding fundamentals and practice. Search for python tutorials, js videos, play with examples, this is how other people climb up the learning curve. No shortcuts, if you want to learn something. What is required to get help? When asking a question: Use the search button for duplicates first! Post your request and response in verbose mode! Add exchange.verbose = true right before the line you're having issues with, and copypaste what you see on your screen. It's written and mentioned everywhere, in the Troubleshooting section, in the README and in many answers to similar questions among previous issues and pull requests. No excuses. The verbose output should include both the request and response from the exchange. Include the full error callstack! Write your programming language and language version number Write the CCXT / CCXT Pro library version number Which exchange it is Which method you're trying to call Post your code to reproduce the problem. Make it a complete short runnable program, don't swallow the lines and make it as compact as you can (5-10 lines of code), including the exchange instantation code. Remove all irrelevant parts from it, leaving just the essence of the code to reproduce the issue. DON'T POST SCREENSHOTS OF CODE OR ERRORS, POST THE OUTPUT AND CODE IN PLAIN TEXT! Surround code and output with triple backticks: ```GOOD```. Don't confuse the backtick symbol (`) with the quote symbol ('): '''BAD''' Don't confuse a single backtick with triple backticks: `BAD` DO NOT POST YOUR apiKey AND secret! Keep them safe (remove them before posting)! I am calling a method and I get an error, what am I doing wrong? You're not reporting the issue properly ) Please, help the community to help you ) Read this and follow the steps: https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-submit-an-issue. Once again, your code to reproduce the issue and your verbose request and response ARE REQUIRED. Just the error traceback, or just the response, or just the request, or just the code – is not enough! I got an incorrect result from a method call, can you help? Basically the same answer as the previous question. Read and follow precisely: https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-submit-an-issue. Once again, your code to reproduce the issue and your verbose request and response ARE REQUIRED. Just the error traceback, or just the response, or just the request, or just the code – is not enough! Can you implement feature foo in exchange bar? Yes, we can. And we will, if nobody else does that before us. There's very little point in asking this type of questions, because the answer is always positive. When someone asks if we can do this or that, the question is not about our abilities, it all boils down to time and management needed for implementing all accumulated feature requests. Moreover, this is an open-source library which is a work in progress. This means, that this project is intended to be developed by the community of users, who are using it. What you're asking is not whether we can or cannot implement it, in fact you're actually telling us to go do that particular task and this is not how we see a voluntary collaboration. Your contributions, PRs and commits are welcome: https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code. We don't give promises or estimates on the free open-source work. If you wish to speed it up, feel free to reach out to us via info@ccxt.trade. When will you add feature foo for exchange bar ? What's the estimated time? When should we expect this? We don't give promises or estimates on the open-source work. The reasoning behind this is explained in the previous paragraph. When will you add the support for an exchange requested in the Issues? Again, we can't promise on the dates for adding this or that exchange, due to reasons outlined above. The answer will always remain the same: as soon as we can. How long should I wait for a feature to be added? I need to decide whether to implement it myself or to wait for the CCXT Dev Team to implement it for me. Please, go for implemeting it yourself, do not wait for us. We will add it as soon as we can. Also, your contributions are very welcome: https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code What's your progress on adding the feature foo that was requested earlier? How do you do implementing exchange bar? This type of questions is usually a waste of time, because answering it usually requires too much time for context-switching, and it often takes more time to answer this question, than to actually satisfy the request with code for a new feature or a new exchange. The progress of this open-source project is also open, so, whenever you're wondering how it is doing, take a look into commit history. What is the status of this PR? Any update? If it is not merged, it means that the PR contains errors, that should be fixed first. If it could be merged as is – we would merge it, and you wouldn't have asked this question in the first place. The most frequent reason for not merging a PR is a violation of any of the CONTRIBUTING guidelines. Those guidelines should be taken literally, cannot skip a single line or word from there if you want your PR to be merged quickly. Code contributions that do not break the guidelines get merged almost immediately (usually, within hours). Can you point out the errors or what should I edit in my PR to get it merged into master branch? Unfortunately, we don't always have the time to quickly list out each and every single error in the code that prevents it from merging. It is often easier and faster to just go and fix the error rather than explain what one should do to fix it. Most of them are already outlined in the CONTRIBUTING guidelines. The main rule of thumb is to follow all guidelines literally. Hey! The fix you've uploaded is in TypeScript, would you fix JavaScript / Python / PHP as well, please? Our build system generates exchange-specific JavaScript, Python and PHP code for us automatically, so it is transpiled from TypeScript, and there's no need to fix all languages separately one by one. Thus, if it is fixed in TypeScript, it is fixed in JavaScript NPM, Python pip and PHP Composer as well. The automatic build usually takes 15-20 minutes. Just upgrade your version with npm, pip or composer after the new version arrives and you'll be fine. More about it here: https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#multilanguage-support https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#transpiled-generated-files How to create an order with takeProfit+stopLoss? Some exchanges support createOrder with the additional "attached" stopLoss & takeProfit sub-orders - view StopLoss And TakeProfit Orders Attached To A Position. However, some exchanges might not support that feature and you will need to run separate createOrder methods to add conditional order (e.g. *trigger order | stoploss order | takeprofit order) to the already open position - view [Conditional orders](Manual.md#Conditional Orders). You can also check them by looking at exchange.has['createOrderWithTakeProfitAndStopLoss'], exchange.has['createStopLossOrder'] and exchange.has['createTakeProfitOrder'], however they are not as precise as .features property. How to create a spot market buy with cost? To create a market-buy order with cost, first, you need to check if the exchange supports that feature (exchange.has['createMarketBuyOrderWithCost']). If it does, then you can use the createMarketBuyOrderWithCost` method. Example: order = await exchange.createMarketBuyOrderWithCost(symbol, cost) What does the createMarketBuyRequiresPrice option mean? Many exchanges require the amount to be in the quote currency (they don't accept the base amount) when placing spot-market buy orders. In those cases, the exchange will have the option createMarketBuyRequiresPrice set to true. Example: If you wanted to buy BTC/USDT with a market buy-order, you would need to provide an amount = 5 USDT instead of 0.000X. We have a check to prevent errors that explicitly require the price because users will usually provide the amount in the base currency. So by default, if you do, create_order(symbol, 'market,' 'buy,' 10) will throw an error if the exchange has that option (createOrder() requires the price argument for market buy orders to calculate the total cost to spend (amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to false...). If the exchange requires the cost and the user provided the base amount, we need to request an extra parameter price and multiply them to get the cost. If you're aware of this behavior, you can simply disable createMarketBuyOrderRequiresPrice and pass the cost in the amount parameter, but disabling it does not mean you can place the order using the base amount instead of the quote. If you do create_order(symbol, 'market', 'buy', 0.001, 20000) ccxt will use the required price to calculate the cost by doing 0.01*20000 and send that value to the exchange. If you want to provide the cost directly in the amount argument, you can do exchange.options['createMarketBuyOrderRequiresPrice'] = False (you acknowledge that the amount will be the cost for market-buy) and then you can do create_order(symbol, 'market', 'buy', 10) This is basically to avoid a user doing this: create_order('SHIB/USDT', market, buy, 1000000) and thinking he's trying to buy 1kk of shib but in reality he's buying 1kk USDT worth of SHIB. For that reason, by default ccxt always accepts the base currency in the amount parameter. Alternatively, you can use the functions createMarketBuyOrderWithCost/ createMarketSellOrderWithCost if they are available. See more: Market Buys What's the difference between trading spot and swap/perpetual futures? Spot trading involves buying or selling a financial instrument (like a cryptocurrency) for immediate delivery. It's straightforward, involving the direct exchange of assets. Swap trading, on the other hand, involves derivative contracts where two parties exchange financial instruments or cash flows at a set date in the future, based on the underlying asset. Swaps are often used for leverage, speculation, or hedging and do not necessarily involve the exchange of the underlying asset until the contract expires. Besides that, you will be handling contracts if you're trading swaps and not the base currency (e.g., BTC) directly, so if you create an order with amount = 1, the amount in BTC will vary depending on the contractSize. You can check the contract size by doing: await exchange.loadMarkets() symbol = 'XRP/USDT:USDT' market = exchange.market(symbol) print(market['contractSize']) How to place a reduceOnly order? A reduceOnly order is a type of order that can only reduce a position, not increase it. To place a reduceOnly order, you typically use the createOrder method with a reduceOnly parameter set to true. This ensures that the order will only execute if it decreases the size of an open position, and it will either partially fill or not fill at all if executing it would increase the position size. Javascript const params = { 'reduceOnly': true, // set to true if you want to close a position, set to false if you want to open a new position } const order = await exchange.createOrder (symbol, type, side, amount, price, params) Python params = { 'reduceOnly': True, # set to True if you want to close a position, set to False if you want to open a new position } order = exchange.create_order (symbol, type, side, amount, price, params) PHP $params = { 'reduceOnly': true, // set to true if you want to close a position, set to false if you want to open a new position } $order = $exchange->create_order ($symbol, $type, $side, $amount, $price, $params); See more: Trailing Orders How to check the endpoint used by the unified method? To check the endpoint used by a unified method in the CCXT library, you would typically need to refer to the source code of the library for the specific exchange implementation you're interested in. The unified methods in CCXT abstract away the details of the specific endpoints they interact with, so this information is not directly exposed via the library's API. For detailed inspection, you can look at the implementation of the method for the particular exchange in the CCXT library's source code on GitHub. See more: Unified API How to differentiate between previousFundingRate, fundingRate and nextFundingRate in the funding rate structure? The funding rate structure has three different funding rate values that can be returned: previousFundingRaterefers to the most recently completed rate. fundingRate is the upcoming rate. This value is always changing until the funding time passes and then it becomes the previousFundingRate. nextFundingRate is only supported on a few exchanges and is the predicted funding rate after the upcoming rate. This value is two funding rates from now. As an example, say it is 12:30. The previousFundingRate happened at 12:00 and we're looking to see what the upcoming funding rate will be by checking the fundingRate value. In this example, given 4-hour intervals, the fundingRate will happen in the future at 4:00 and the nextFundingRate is the predicted rate that will happen at 8:00.
-
-```
-python tutorials
-```
-
-**Pattern 2:** To create a market-buy order with cost, first, you need to check if the exchange supports that feature (exchange.has['createMarketBuyOrderWithCost']). If it does, then you can use the createMarketBuyOrderWithCost` method. Example:
-
-```
-exchange.has['createMarketBuyOrderWithCost']). If it does, then you can use the
-```
-
-**Pattern 3:** Example: If you wanted to buy BTC/USDT with a market buy-order, you would need to provide an amount = 5 USDT instead of 0.000X. We have a check to prevent errors that explicitly require the price because users will usually provide the amount in the base currency.
-
-```
-create_order(symbol, 'market,' 'buy,' 10)
-```
-
-**Pattern 4:** For a complete list of all exchanges and their supported methods, please, refer to this example: https://github.com/ccxt/ccxt/blob/master/examples/js/exchange-capabilities.js
-
-```
-exchange.rateLimit
-```
-
-**Pattern 5:** The ccxt library supports asynchronous concurrency mode in Python 3.5+ with async/await syntax. The asynchronous Python version uses pure asyncio with aiohttp. In async mode you have all the same properties and methods, but most methods are decorated with an async keyword. If you want to use async mode, you should link against the ccxt.async_support subpackage, like in the following example:
-
-```
-ccxt.async_support
-```
-
-## Reference Files
-
-This skill includes comprehensive documentation in `references/`:
-
-- **cli.md** - Cli documentation
-- **exchanges.md** - Exchanges documentation
-- **faq.md** - Faq documentation
-- **getting_started.md** - Getting Started documentation
-- **manual.md** - Manual documentation
-- **other.md** - Other documentation
-- **pro.md** - Pro documentation
-- **specification.md** - Specification documentation
-
-Use `view` to read specific reference files when detailed information is needed.
-
-## Working with This Skill
-
-### For Beginners
-Start with the getting_started or tutorials reference files for foundational concepts.
-
-### For Specific Features
-Use the appropriate category reference file (api, guides, etc.) for detailed information.
-
-### For Code Examples
-The quick reference section above contains common patterns extracted from the official docs.
-
-## Resources
-
-### references/
-Organized documentation extracted from official sources. These files contain:
-- Detailed explanations
-- Code examples with language annotations
-- Links to original documentation
-- Table of contents for quick navigation
-
-### scripts/
-Add helper scripts here for common automation tasks.
-
-### assets/
-Add templates, boilerplate, or example projects here.
-
-## Notes
-
-- This skill was automatically generated from official documentation
-- Reference files preserve the structure and examples from source docs
-- Code examples include language detection for better syntax highlighting
-- Quick reference patterns are extracted from common usage examples in the docs
-
-## Updating
-
-To refresh this skill with updated documentation:
-1. Re-run the scraper with the same configuration
-2. The skill will be rebuilt with the latest information
diff --git a/assets/skills/claude-code-guide/SKILL.md b/assets/skills/claude-code-guide/SKILL.md
deleted file mode 100644
index 780b39d..0000000
--- a/assets/skills/claude-code-guide/SKILL.md
+++ /dev/null
@@ -1,470 +0,0 @@
----
-name: claude-code-guide
-description: Claude Code 高级开发指南 - 全面的中文教程,涵盖工具使用、REPL 环境、开发工作流、MCP 集成、高级模式和最佳实践。适合学习 Claude Code 的高级功能和开发技巧。
----
-
-# Claude Code 高级开发指南
-
-全面的 Claude Code 中文学习指南,涵盖从基础到高级的所有核心概念、工具使用、开发工作流和最佳实践。
-
-## 何时使用此技能
-
-当需要以下帮助时使用此技能:
-- 学习 Claude Code 的核心功能和工具
-- 掌握 REPL 环境的高级用法
-- 理解开发工作流和任务管理
-- 使用 MCP 集成外部系统
-- 实现高级开发模式
-- 应用 Claude Code 最佳实践
-- 解决常见问题和错误
-- 进行大文件分析和处理
-
-## 快速参考
-
-### Claude Code 核心工具(7个)
-
-1. **REPL** - JavaScript 运行时环境
- - 完整的 ES6+ 支持
- - 预加载库:D3.js, MathJS, Lodash, Papaparse, SheetJS
- - 支持 async/await, BigInt, WebAssembly
- - 文件读取:`window.fs.readFile()`
-
-2. **Artifacts** - 可视化输出
- - React, Three.js, 图表库
- - HTML/SVG 渲染
- - 交互式组件
-
-3. **Web Search** - 网络搜索
- - 仅美国可用
- - 域名过滤支持
-
-4. **Web Fetch** - 获取网页内容
- - HTML 转 Markdown
- - 内容提取和分析
-
-5. **Conversation Search** - 对话搜索
- - 搜索历史对话
- - 上下文检索
-
-6. **Recent Chats** - 最近对话
- - 访问最近会话
- - 对话历史
-
-7. **End Conversation** - 结束对话
- - 清理和总结
- - 会话管理
-
-### 大文件分析工作流
-
-```bash
-# 阶段 1:定量评估
-wc -l filename.md # 行数统计
-wc -w filename.md # 词数统计
-wc -c filename.md # 字符数统计
-
-# 阶段 2:结构分析
-grep "^#{1,6} " filename.md # 提取标题层次
-grep "```" filename.md # 识别代码块
-grep -c "keyword" filename.md # 关键词频率
-
-# 阶段 3:内容提取
-Read filename.md offset=0 limit=50 # 文件开头
-Read filename.md offset=N limit=100 # 目标部分
-Read filename.md offset=-50 limit=50 # 文件结尾
-```
-
-### REPL 高级用法
-
-```javascript
-// 数据处理
-const data = [1, 2, 3, 4, 5];
-const sum = data.reduce((a, b) => a + b, 0);
-
-// 使用预加载库
-// Lodash
-_.chunk([1, 2, 3, 4], 2); // [[1,2], [3,4]]
-
-// MathJS
-math.sqrt(16); // 4
-
-// D3.js
-d3.range(10); // [0,1,2,3,4,5,6,7,8,9]
-
-// 读取文件
-const content = await window.fs.readFile('path/to/file');
-
-// 异步操作
-const result = await fetch('https://api.example.com/data');
-const json = await result.json();
-```
-
-### 斜杠命令系统
-
-**内置命令:**
-- `/help` - 显示帮助
-- `/clear` - 清除对话
-- `/plugin` - 管理插件
-- `/settings` - 配置设置
-
-**自定义命令:**
-创建 `.claude/commands/mycommand.md`:
-```markdown
-根据需求执行特定任务的指令
-```
-
-使用:`/mycommand`
-
-### 开发工作流模式
-
-#### 1. 文件分析工作流
-```bash
-# 探索 → 理解 → 实现
-ls -la # 列出文件
-Read file.py # 读取内容
-grep "function" file.py # 搜索模式
-# 然后实现修改
-```
-
-#### 2. 算法验证工作流
-```bash
-# 设计 → 验证 → 实现
-# 1. 在 REPL 中测试逻辑
-# 2. 验证边界情况
-# 3. 实现到代码
-```
-
-#### 3. 数据探索工作流
-```bash
-# 检查 → 分析 → 可视化
-# 1. 读取数据文件
-# 2. REPL 中分析
-# 3. Artifacts 可视化
-```
-
-## 核心概念
-
-### 工具权限系统
-
-**自动授予权限的工具:**
-- REPL
-- Artifacts
-- Web Search/Fetch
-- Conversation Search
-
-**需要授权的工具:**
-- Bash (读/写文件系统)
-- Edit (修改文件)
-- Write (创建文件)
-
-### 项目上下文
-
-Claude 自动识别:
-- Git 仓库状态
-- 编程语言(从文件扩展名)
-- 项目结构
-- 依赖配置
-
-### 内存系统
-
-**对话内存:**
-- 存储在当前会话
-- 200K token 窗口
-- 自动上下文管理
-
-**持久内存(实验性):**
-- 跨会话保存
-- 用户偏好记忆
-- 项目上下文保留
-
-## MCP 集成
-
-### 什么是 MCP?
-
-Model Context Protocol - 连接 Claude 到外部系统的协议。
-
-### MCP 服务器配置
-
-配置文件:`~/.config/claude/mcp_config.json`
-
-```json
-{
- "mcpServers": {
- "my-server": {
- "command": "node",
- "args": ["path/to/server.js"],
- "env": {
- "API_KEY": "your-key"
- }
- }
- }
-}
-```
-
-### 使用 MCP 工具
-
-Claude 会自动发现 MCP 工具并在对话中使用:
-
-```
-"使用 my-server 工具获取数据"
-```
-
-## 钩子系统
-
-### 钩子类型
-
-在 `.claude/settings.json` 配置:
-
-```json
-{
- "hooks": {
- "tool-pre-use": "echo 'About to use tool'",
- "tool-post-use": "echo 'Tool used'",
- "user-prompt-submit": "echo 'Processing prompt'"
- }
-}
-```
-
-### 常见钩子用途
-
-- 自动格式化代码
-- 运行测试
-- Git 提交检查
-- 日志记录
-- 通知发送
-
-## 高级模式
-
-### 多代理协作
-
-使用 Task 工具启动子代理:
-
-```
-"启动一个专门的代理来优化这个算法"
-```
-
-子代理特点:
-- 独立上下文
-- 专注单一任务
-- 返回结果到主代理
-
-### 智能任务管理
-
-使用 TodoWrite 工具:
-
-```
-"创建任务列表来跟踪这个项目"
-```
-
-任务状态:
-- `pending` - 待处理
-- `in_progress` - 进行中
-- `completed` - 已完成
-
-### 代码生成模式
-
-**渐进式开发:**
-1. 生成基础结构
-2. 添加核心功能
-3. 实现细节
-4. 测试和优化
-
-**验证驱动:**
-1. 写测试用例
-2. 实现功能
-3. 运行测试
-4. 修复问题
-
-## 质量保证
-
-### 自动化测试
-
-```bash
-# 运行测试
-npm test
-pytest
-
-# 类型检查
-mypy script.py
-tsc --noEmit
-
-# 代码检查
-eslint src/
-flake8 .
-```
-
-### 代码审查模式
-
-使用子代理进行审查:
-
-```
-"启动代码审查代理检查这个文件"
-```
-
-审查重点:
-- 代码质量
-- 安全问题
-- 性能优化
-- 最佳实践
-
-## 错误恢复
-
-### 常见错误模式
-
-1. **工具使用错误**
- - 检查权限
- - 验证语法
- - 确认路径
-
-2. **文件操作错误**
- - 确认文件存在
- - 检查读写权限
- - 验证路径正确
-
-3. **API 调用错误**
- - 检查网络连接
- - 验证 API 密钥
- - 确认请求格式
-
-### 渐进式修复策略
-
-1. 隔离问题
-2. 最小化复现
-3. 逐步修复
-4. 验证解决方案
-
-## 最佳实践
-
-### 开发原则
-
-1. **清晰优先** - 明确需求和目标
-2. **渐进实现** - 分步骤开发
-3. **持续验证** - 频繁测试
-4. **适当抽象** - 合理模块化
-
-### 工具使用原则
-
-1. **正确的工具** - 选择合适的工具
-2. **工具组合** - 多工具协同
-3. **权限最小化** - 只请求必要权限
-4. **错误处理** - 优雅处理失败
-
-### 性能优化
-
-1. **批量操作** - 合并多个操作
-2. **增量处理** - 处理大文件
-3. **缓存结果** - 避免重复计算
-4. **异步优先** - 使用 async/await
-
-## 安全考虑
-
-### 沙箱模型
-
-每个工具在隔离环境中运行:
-- REPL:无文件系统访问
-- Bash:需要明确授权
-- Web:仅特定域名
-
-### 最佳安全实践
-
-1. **最小权限** - 仅授予必要权限
-2. **代码审查** - 检查生成的代码
-3. **敏感数据** - 不要共享密钥
-4. **定期审计** - 检查钩子和配置
-
-## 故障排除
-
-### 工具无法使用
-
-**症状:** 工具调用失败
-
-**解决方案:**
-- 检查权限设置
-- 验证语法正确
-- 确认文件路径
-- 查看错误消息
-
-### REPL 性能问题
-
-**症状:** REPL 执行缓慢
-
-**解决方案:**
-- 减少数据量
-- 使用流式处理
-- 优化算法
-- 分批处理
-
-### MCP 连接失败
-
-**症状:** MCP 服务器无响应
-
-**解决方案:**
-- 检查配置文件
-- 验证服务器运行
-- 确认环境变量
-- 查看服务器日志
-
-## 实用示例
-
-### 示例 1:数据分析
-
-```javascript
-// 在 REPL 中
-const data = await window.fs.readFile('data.csv');
-const parsed = Papa.parse(data, { header: true });
-const values = parsed.data.map(row => parseFloat(row.value));
-const avg = _.mean(values);
-const std = math.std(values);
-console.log(`平均值: ${avg}, 标准差: ${std}`);
-```
-
-### 示例 2:文件搜索
-
-```bash
-# 在 Bash 中
-grep -r "TODO" src/
-find . -name "*.py" -type f
-```
-
-### 示例 3:网络数据获取
-
-```
-"使用 web_fetch 获取 https://api.example.com/data 的内容,
-然后在 REPL 中分析 JSON 数据"
-```
-
-## 参考文件
-
-此技能包含详细文档:
-
-- **README.md** (9,594 行) - 完整的 Claude Code 高级指南
-
-包含以下主题:
-- 核心工具深度解析
-- REPL 高级协同模式
-- 开发工作流详解
-- MCP 集成完整指南
-- 钩子系统配置
-- 高级模式和最佳实践
-- 故障排除和安全考虑
-
-使用 `view` 命令查看参考文件获取详细信息。
-
-## 资源
-
-- **GitHub 仓库**: https://github.com/karminski/claude-code-guide-study
-- **原始版本**: https://github.com/Cranot/claude-code-guide
-- **Anthropic 官方文档**: https://docs.claude.com
-
-## 注意事项
-
-本指南结合了:
-- 官方功能和公告
-- 实际使用观察到的模式
-- 概念性方法和最佳实践
-- 第三方工具集成
-
-请在使用时参考最新的官方文档。
-
----
-
-**使用这个技能深入掌握 Claude Code 的强大功能!**
diff --git a/assets/skills/claude-cookbooks/SKILL.md b/assets/skills/claude-cookbooks/SKILL.md
deleted file mode 100644
index d64a87b..0000000
--- a/assets/skills/claude-cookbooks/SKILL.md
+++ /dev/null
@@ -1,313 +0,0 @@
----
-name: claude-cookbooks
-description: Claude AI cookbooks - code examples, tutorials, and best practices for using Claude API. Use when learning Claude API integration, building Claude-powered applications, or exploring Claude capabilities.
----
-
-# Claude Cookbooks Skill
-
-Comprehensive code examples and guides for building with Claude AI, sourced from the official Anthropic cookbooks repository.
-
-## When to Use This Skill
-
-This skill should be triggered when:
-- Learning how to use Claude API
-- Implementing Claude integrations
-- Building applications with Claude
-- Working with tool use and function calling
-- Implementing multimodal features (vision, image analysis)
-- Setting up RAG (Retrieval Augmented Generation)
-- Integrating Claude with third-party services
-- Building AI agents with Claude
-- Optimizing prompts for Claude
-- Implementing advanced patterns (caching, sub-agents, etc.)
-
-## Quick Reference
-
-### Basic API Usage
-
-```python
-import anthropic
-
-client = anthropic.Anthropic(api_key="your-api-key")
-
-# Simple message
-response = client.messages.create(
- model="claude-3-5-sonnet-20241022",
- max_tokens=1024,
- messages=[{
- "role": "user",
- "content": "Hello, Claude!"
- }]
-)
-```
-
-### Tool Use (Function Calling)
-
-```python
-# Define a tool
-tools = [{
- "name": "get_weather",
- "description": "Get current weather for a location",
- "input_schema": {
- "type": "object",
- "properties": {
- "location": {"type": "string", "description": "City name"}
- },
- "required": ["location"]
- }
-}]
-
-# Use the tool
-response = client.messages.create(
- model="claude-3-5-sonnet-20241022",
- max_tokens=1024,
- tools=tools,
- messages=[{"role": "user", "content": "What's the weather in San Francisco?"}]
-)
-```
-
-### Vision (Image Analysis)
-
-```python
-# Analyze an image
-response = client.messages.create(
- model="claude-3-5-sonnet-20241022",
- max_tokens=1024,
- messages=[{
- "role": "user",
- "content": [
- {
- "type": "image",
- "source": {
- "type": "base64",
- "media_type": "image/jpeg",
- "data": base64_image
- }
- },
- {"type": "text", "text": "Describe this image"}
- ]
- }]
-)
-```
-
-### Prompt Caching
-
-```python
-# Use prompt caching for efficiency
-response = client.messages.create(
- model="claude-3-5-sonnet-20241022",
- max_tokens=1024,
- system=[{
- "type": "text",
- "text": "Large system prompt here...",
- "cache_control": {"type": "ephemeral"}
- }],
- messages=[{"role": "user", "content": "Your question"}]
-)
-```
-
-## Key Capabilities Covered
-
-### 1. Classification
-- Text classification techniques
-- Sentiment analysis
-- Content categorization
-- Multi-label classification
-
-### 2. Retrieval Augmented Generation (RAG)
-- Vector database integration
-- Semantic search
-- Context retrieval
-- Knowledge base queries
-
-### 3. Summarization
-- Document summarization
-- Meeting notes
-- Article condensing
-- Multi-document synthesis
-
-### 4. Text-to-SQL
-- Natural language to SQL queries
-- Database schema understanding
-- Query optimization
-- Result interpretation
-
-### 5. Tool Use & Function Calling
-- Tool definition and schema
-- Parameter validation
-- Multi-tool workflows
-- Error handling
-
-### 6. Multimodal
-- Image analysis and OCR
-- Chart/graph interpretation
-- Visual question answering
-- Image generation integration
-
-### 7. Advanced Patterns
-- Agent architectures
-- Sub-agent delegation
-- Prompt optimization
-- Cost optimization with caching
-
-## Repository Structure
-
-The cookbooks are organized into these main categories:
-
-- **capabilities/** - Core AI capabilities (classification, RAG, summarization, text-to-SQL)
-- **tool_use/** - Function calling and tool integration examples
-- **multimodal/** - Vision and image-related examples
-- **patterns/** - Advanced patterns like agents and workflows
-- **third_party/** - Integrations with external services (Pinecone, LlamaIndex, etc.)
-- **claude_agent_sdk/** - Agent SDK examples and templates
-- **misc/** - Additional utilities (PDF upload, JSON mode, evaluations, etc.)
-
-## Reference Files
-
-This skill includes comprehensive documentation in `references/`:
-
-- **main_readme.md** - Main repository overview
-- **capabilities.md** - Core capabilities documentation
-- **tool_use.md** - Tool use and function calling guides
-- **multimodal.md** - Vision and multimodal capabilities
-- **third_party.md** - Third-party integrations
-- **patterns.md** - Advanced patterns and agents
-- **index.md** - Complete reference index
-
-## Common Use Cases
-
-### Building a Customer Service Agent
-1. Define tools for CRM access, ticket creation, knowledge base search
-2. Use tool use API to handle function calls
-3. Implement conversation memory
-4. Add fallback mechanisms
-
-See: `references/tool_use.md#customer-service`
-
-### Implementing RAG
-1. Create embeddings of your documents
-2. Store in vector database (Pinecone, etc.)
-3. Retrieve relevant context on query
-4. Augment Claude's response with context
-
-See: `references/capabilities.md#rag`
-
-### Processing Documents with Vision
-1. Convert document to images or PDF
-2. Use vision API to extract content
-3. Structure the extracted data
-4. Validate and post-process
-
-See: `references/multimodal.md#vision`
-
-### Building Multi-Agent Systems
-1. Define specialized agents for different tasks
-2. Implement routing logic
-3. Use sub-agents for delegation
-4. Aggregate results
-
-See: `references/patterns.md#agents`
-
-## Best Practices
-
-### API Usage
-- Use appropriate model for task (Sonnet for balance, Haiku for speed, Opus for complex tasks)
-- Implement retry logic with exponential backoff
-- Handle rate limits gracefully
-- Monitor token usage for cost optimization
-
-### Prompt Engineering
-- Be specific and clear in instructions
-- Provide examples when needed
-- Use system prompts for consistent behavior
-- Structure outputs with JSON mode when needed
-
-### Tool Use
-- Define clear, specific tool schemas
-- Validate inputs and outputs
-- Handle errors gracefully
-- Keep tool descriptions concise but informative
-
-### Multimodal
-- Use high-quality images (higher resolution = better results)
-- Be specific about what to extract/analyze
-- Respect size limits (5MB per image)
-- Use appropriate image formats (JPEG, PNG, GIF, WebP)
-
-## Performance Optimization
-
-### Prompt Caching
-- Cache large system prompts
-- Cache frequently used context
-- Monitor cache hit rates
-- Balance caching vs. fresh content
-
-### Cost Optimization
-- Use Haiku for simple tasks
-- Implement prompt caching for repeated context
-- Set appropriate max_tokens
-- Batch similar requests
-
-### Latency Optimization
-- Use streaming for long responses
-- Minimize message history
-- Optimize image sizes
-- Use appropriate timeout values
-
-## Resources
-
-### Official Documentation
-- [Anthropic Developer Docs](https://docs.claude.com)
-- [API Reference](https://docs.claude.com/claude/reference)
-- [Anthropic Support](https://support.anthropic.com)
-
-### Community
-- [Anthropic Discord](https://www.anthropic.com/discord)
-- [GitHub Cookbooks Repo](https://github.com/anthropics/claude-cookbooks)
-
-### Learning Resources
-- [Claude API Fundamentals Course](https://github.com/anthropics/courses/tree/master/anthropic_api_fundamentals)
-- [Prompt Engineering Guide](https://docs.claude.com/claude/docs/guide-to-anthropics-prompt-engineering-resources)
-
-## Working with This Skill
-
-### For Beginners
-Start with `references/main_readme.md` and explore basic examples in `references/capabilities.md`
-
-### For Specific Features
-- Tool use → `references/tool_use.md`
-- Vision → `references/multimodal.md`
-- RAG → `references/capabilities.md#rag`
-- Agents → `references/patterns.md#agents`
-
-### For Code Examples
-Each reference file contains practical, copy-pasteable code examples
-
-## Examples Available
-
-The cookbook includes 50+ practical examples including:
-- Customer service chatbot with tool use
-- RAG with Pinecone vector database
-- Document summarization
-- Image analysis and OCR
-- Chart/graph interpretation
-- Natural language to SQL
-- Content moderation filter
-- Automated evaluations
-- Multi-agent systems
-- Prompt caching optimization
-
-## Notes
-
-- All examples use official Anthropic Python SDK
-- Code is production-ready with error handling
-- Examples follow current API best practices
-- Regular updates from Anthropic team
-- Community contributions welcome
-
-## Skill Source
-
-This skill was created from the official Anthropic Claude Cookbooks repository:
-https://github.com/anthropics/claude-cookbooks
-
-Repository cloned and processed on: 2025-10-29
diff --git a/assets/skills/claude-cookbooks/references/capabilities.md b/assets/skills/claude-cookbooks/references/capabilities.md
deleted file mode 100644
index 27c585a..0000000
--- a/assets/skills/claude-cookbooks/references/capabilities.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Claude Capabilities
-
-Welcome to the Capabilities section of the Claude Cookbooks! This directory contains a collection of guides that showcase specific capabilities where Claude excels. Each guide provides an in-depth exploration of a particular capability, discussing potential use cases, prompt engineering techniques to optimize results, and approaches for evaluating Claude's performance.
-
-## Guides
-
-- **[Classification with Claude](./classification/guide.ipynb)**: Discover how Claude can revolutionize classification tasks, especially in scenarios with complex business rules and limited training data. This guide walks you through data preparation, prompt engineering with retrieval-augmented generation (RAG), testing, and evaluation.
-
-- **[Retrieval Augmented Generation with Claude](./retrieval_augmented_generation/guide.ipynb)**: Learn how to enhance Claude's capabilities with domain-specific knowledge using RAG. This guide demonstrates how to build a RAG system from scratch, optimize its performance, and create an evaluation suite. You'll learn how techniques like summary indexing and re-ranking can significantly improve precision, recall, and overall accuracy in question-answering tasks.
-
-- **[Retrieval Augmented Generation with Contextual Embeddings](./contextual-embeddings/guide.ipynb)**: Learn how to use a new technique to improve the performance of your RAG system. In traditional RAG, documents are typically split into smaller chunks for efficient retrieval. While this approach works well for many applications, it can lead to problems when individual chunks lack sufficient context. Contextual Embeddings solve this problem by adding relevant context to each chunk before embedding. You'll learn how to use contextual embeddings with semantic search, BM25 search, and reranking to improve performance.
-
-- **[Summarization with Claude](./summarization/guide.ipynb)**: Explore Claude's ability to summarize and synthesize information from multiple sources. This guide covers a variety of summarization techniques, including multi-shot, domain-based, and chunking methods, as well as strategies for handling long-form content and multiple documents. We also explore evaluating summaries, which can be a balance of art, subjectivity, and the right approach!
-
-- **[Text-to-SQL with Claude](./text_to_sql/guide.ipynb)**: This guide covers how to generate complex SQL queries from natural language using prompting techniques, self-improvement, and RAG. We'll also explore how to evaluate and improve the accuracy of generated SQL queries, with evals that test for syntax, data correctness, row count, and more.
-
-## Getting Started
-
-To get started with these guides, simply navigate to the desired guide's directory and follow the instructions provided in the `guide.ipynb` file. Each guide is self-contained and includes all the necessary code, data, and evaluation scripts to reproduce the examples and experiments.
\ No newline at end of file
diff --git a/assets/skills/claude-official-skills b/assets/skills/claude-official-skills
deleted file mode 120000
index 315d26e..0000000
--- a/assets/skills/claude-official-skills
+++ /dev/null
@@ -1 +0,0 @@
-../repo/claude-official-skills
\ No newline at end of file
diff --git a/assets/skills/coingecko/SKILL.md b/assets/skills/coingecko/SKILL.md
deleted file mode 100644
index 8259d9c..0000000
--- a/assets/skills/coingecko/SKILL.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-name: coingecko
-description: CoinGecko API documentation - cryptocurrency market data API, price feeds, market cap, volume, historical data. Use when integrating CoinGecko API, building crypto price trackers, or accessing cryptocurrency market data.
----
-
-# Coingecko Skill
-
-Comprehensive assistance with coingecko development, generated from official documentation.
-
-## When to Use This Skill
-
-This skill should be triggered when:
-- Working with coingecko
-- Asking about coingecko features or APIs
-- Implementing coingecko solutions
-- Debugging coingecko code
-- Learning coingecko best practices
-
-## Quick Reference
-
-### Common Patterns
-
-*Quick reference patterns will be added as you use the skill.*
-
-## Reference Files
-
-This skill includes comprehensive documentation in `references/`:
-
-- **authentication.md** - Authentication documentation
-- **coins.md** - Coins documentation
-- **contract.md** - Contract documentation
-- **exchanges.md** - Exchanges documentation
-- **introduction.md** - Introduction documentation
-- **market_data.md** - Market Data documentation
-- **nfts.md** - Nfts documentation
-- **other.md** - Other documentation
-- **pricing.md** - Pricing documentation
-- **reference.md** - Reference documentation
-- **trending.md** - Trending documentation
-
-Use `view` to read specific reference files when detailed information is needed.
-
-## Working with This Skill
-
-### For Beginners
-Start with the getting_started or tutorials reference files for foundational concepts.
-
-### For Specific Features
-Use the appropriate category reference file (api, guides, etc.) for detailed information.
-
-### For Code Examples
-The quick reference section above contains common patterns extracted from the official docs.
-
-## Resources
-
-### references/
-Organized documentation extracted from official sources. These files contain:
-- Detailed explanations
-- Code examples with language annotations
-- Links to original documentation
-- Table of contents for quick navigation
-
-### scripts/
-Add helper scripts here for common automation tasks.
-
-### assets/
-Add templates, boilerplate, or example projects here.
-
-## Notes
-
-- This skill was automatically generated from official documentation
-- Reference files preserve the structure and examples from source docs
-- Code examples include language detection for better syntax highlighting
-- Quick reference patterns are extracted from common usage examples in the docs
-
-## Updating
-
-To refresh this skill with updated documentation:
-1. Re-run the scraper with the same configuration
-2. The skill will be rebuilt with the latest information
diff --git a/assets/skills/cryptofeed/SKILL.md b/assets/skills/cryptofeed/SKILL.md
deleted file mode 100644
index b76cf76..0000000
--- a/assets/skills/cryptofeed/SKILL.md
+++ /dev/null
@@ -1,221 +0,0 @@
----
-name: cryptofeed
-description: Cryptofeed - Real-time cryptocurrency market data feeds from 40+ exchanges. WebSocket streaming, normalized data, order books, trades, tickers. Python library for algorithmic trading and market data analysis.
----
-
-# Cryptofeed Skill
-
-Comprehensive assistance with Cryptofeed development - a Python library for handling cryptocurrency exchange data feeds with normalized and standardized results.
-
-## When to Use This Skill
-
-This skill should be triggered when:
-- Working with real-time cryptocurrency market data
-- Implementing WebSocket streaming from crypto exchanges
-- Building algorithmic trading systems
-- Processing order book updates, trades, or ticker data
-- Connecting to 40+ cryptocurrency exchanges
-- Using normalized exchange APIs
-- Implementing market data backends (Redis, MongoDB, Kafka, etc.)
-
-## Quick Reference
-
-### Installation
-
-```python
-# Basic installation
-pip install cryptofeed
-
-# With all optional backends
-pip install cryptofeed[all]
-```
-
-### Basic Usage Pattern
-
-```python
-from cryptofeed import FeedHandler
-from cryptofeed.exchanges import Coinbase, Bitfinex
-from cryptofeed.defines import TICKER, TRADES, L2_BOOK
-
-# Define callbacks
-def ticker_callback(data):
- print(f"Ticker: {data}")
-
-def trade_callback(data):
- print(f"Trade: {data}")
-
-# Create feed handler
-fh = FeedHandler()
-
-# Add exchange feeds
-fh.add_feed(Coinbase(
- symbols=['BTC-USD'],
- channels=[TICKER],
- callbacks={TICKER: ticker_callback}
-))
-
-fh.add_feed(Bitfinex(
- symbols=['BTC-USD'],
- channels=[TRADES],
- callbacks={TRADES: trade_callback}
-))
-
-# Start receiving data
-fh.run()
-```
-
-### National Best Bid/Offer (NBBO)
-
-```python
-from cryptofeed import FeedHandler
-from cryptofeed.exchanges import Coinbase, Gemini, Kraken
-
-def nbbo_update(symbol, bid, bid_size, ask, ask_size, bid_feed, ask_feed):
- print(f'Pair: {symbol} Bid: {bid:.2f} ({bid_size:.6f}) from {bid_feed}')
- print(f'Ask: {ask:.2f} ({ask_size:.6f}) from {ask_feed}')
-
-f = FeedHandler()
-f.add_nbbo([Coinbase, Kraken, Gemini], ['BTC-USD'], nbbo_update)
-f.run()
-```
-
-## Supported Exchanges (40+)
-
-### Major Exchanges
-- **Binance** (Spot, Futures, Delivery, US)
-- **Coinbase**, **Kraken** (Spot, Futures), **Bitfinex**
-- **Gemini**, **OKX**, **Bybit**
-- **Huobi** (Spot, DM, Swap), **Gate.io** (Spot, Futures)
-- **KuCoin**, **Deribit**, **BitMEX**, **dYdX**
-
-### Additional Exchanges
-AscendEX, Bequant, bitFlyer, Bithumb, Bitstamp, Blockchain.com, Bit.com, Bitget, Crypto.com, Delta, EXX, FMFW.io, HitBTC, Independent Reserve, OKCoin, Phemex, Poloniex, ProBit, Upbit
-
-## Supported Data Channels
-
-### Market Data (Public)
-- **L1_BOOK** - Top of order book
-- **L2_BOOK** - Price aggregated sizes
-- **L3_BOOK** - Price aggregated orders
-- **TRADES** - Executed trades (taker side)
-- **TICKER** - Price ticker updates
-- **FUNDING** - Funding rate data
-- **OPEN_INTEREST** - Open interest statistics
-- **LIQUIDATIONS** - Liquidation events
-- **INDEX** - Index price data
-- **CANDLES** - Candlestick/K-line data
-
-### Authenticated Channels (Private)
-- **ORDER_INFO** - Order status updates
-- **TRANSACTIONS** - Deposits and withdrawals
-- **BALANCES** - Wallet balance updates
-- **FILLS** - User's executed trades
-
-## Supported Backends
-
-Write data directly to storage:
-
-- **Redis** (Streams and Sorted Sets)
-- **Arctic** - Time-series database
-- **ZeroMQ**, **InfluxDB v2**, **MongoDB**
-- **Kafka**, **RabbitMQ**, **PostgreSQL**
-- **QuasarDB**, **GCP Pub/Sub**, **QuestDB**
-- **UDP/TCP/Unix Sockets**
-
-## Key Features
-
-### Real-time Data Normalization
-Cryptofeed normalizes data across all exchanges, providing consistent:
-- Symbol formatting
-- Timestamp handling
-- Data structures
-- Channel names
-
-### WebSocket + REST Fallback
-- Primarily uses WebSockets for real-time data
-- Falls back to REST polling when WebSocket unavailable
-- Automatic reconnection handling
-
-### NBBO Aggregation
-Create synthetic National Best Bid/Offer feeds by aggregating data across multiple exchanges to find arbitrage opportunities.
-
-### Backend Integration
-Direct data writing to various storage systems without custom integration code.
-
-## Requirements
-
-- **Python**: 3.8 or higher
-- **Installation**: Via pip or from source
-- **Optional Dependencies**: Install backends as needed
-
-## Common Use Cases
-
-### Multi-Exchange Price Monitoring
-```python
-fh = FeedHandler()
-fh.add_feed(Binance(symbols=['BTC-USDT'], channels=[TICKER], callbacks=ticker_cb))
-fh.add_feed(Coinbase(symbols=['BTC-USD'], channels=[TICKER], callbacks=ticker_cb))
-fh.add_feed(Kraken(symbols=['BTC-USD'], channels=[TICKER], callbacks=ticker_cb))
-fh.run()
-```
-
-### Order Book Depth Analysis
-```python
-def book_callback(book, receipt_timestamp):
- print(f"Bids: {len(book.book.bids)} | Asks: {len(book.book.asks)}")
-
-fh.add_feed(Coinbase(
- symbols=['BTC-USD'],
- channels=[L2_BOOK],
- callbacks={L2_BOOK: book_callback}
-))
-```
-
-### Trade Flow Analysis
-```python
-def trade_callback(trade, receipt_timestamp):
- print(f"{trade.exchange} - {trade.symbol}: {trade.side} {trade.amount} @ {trade.price}")
-
-fh.add_feed(Binance(
- symbols=['BTC-USDT', 'ETH-USDT'],
- channels=[TRADES],
- callbacks={TRADES: trade_callback}
-))
-```
-
-## Reference Files
-
-This skill includes documentation in `references/`:
-
-- **getting_started.md** - Installation and basic usage
-- **README.md** - Complete overview and examples
-
-Use `view` to read specific reference files when detailed information is needed.
-
-## Working with This Skill
-
-### For Beginners
-Start with basic FeedHandler setup and single exchange connections before adding multiple feeds.
-
-### For Advanced Users
-Explore NBBO feeds, authenticated channels, and backend integrations for production systems.
-
-### For Code Examples
-See the quick reference section above and the reference files for complete working examples.
-
-## Resources
-
-- **Repository**: https://github.com/bmoscon/cryptofeed
-- **PyPI**: https://pypi.python.org/pypi/cryptofeed
-- **Examples**: https://github.com/bmoscon/cryptofeed/tree/master/examples
-- **Documentation**: https://github.com/bmoscon/cryptofeed/blob/master/docs/README.md
-- **Discord**: https://discord.gg/zaBYaGAYfR
-- **Related**: Cryptostore (containerized data storage)
-
-## Notes
-
-- Requires Python 3.8+
-- WebSocket-first approach with REST fallback
-- Normalized data across all exchanges
-- Active development and community support
-- 40+ supported exchanges and growing
diff --git a/assets/skills/hummingbot/SKILL.md b/assets/skills/hummingbot/SKILL.md
deleted file mode 100644
index 171e5cd..0000000
--- a/assets/skills/hummingbot/SKILL.md
+++ /dev/null
@@ -1,133 +0,0 @@
----
-name: hummingbot
-description: Hummingbot trading bot framework - automated trading strategies, market making, arbitrage, connectors for crypto exchanges. Use when working with algorithmic trading, crypto trading bots, or exchange integrations.
----
-
-# Hummingbot Skill
-
-Comprehensive assistance with hummingbot development, generated from official documentation.
-
-## When to Use This Skill
-
-This skill should be triggered when:
-- Working with hummingbot
-- Asking about hummingbot features or APIs
-- Implementing hummingbot solutions
-- Debugging hummingbot code
-- Learning hummingbot best practices
-
-## Quick Reference
-
-### Common Patterns
-
-**Pattern 1:** For example: candles = [CandlesFactory.get_candle(connector=kucoin, trading_pair="ETH-USDT", interval="1m", max_records=100)]
-
-```
-candles = [CandlesFactory.get_candle(connector=kucoin,
- trading_pair="ETH-USDT", interval="1m", max_records=100)]
-```
-
-**Pattern 2:** Example:
-
-```
-bin/hummingbot_quickstart.py -p a -f simple_pmm_example_config.py -c conf_simple_pmm_example_config_1.yml
-```
-
-**Pattern 3:** >>> gateway swap --help usage: gateway swap [-h] [connector] [args ...] positional arguments: connector Connector name/type (e.g., jupiter/router) args Arguments: [base-quote] [side] [amount] options: -h, --help show this help message and exit
-
-```
->>> gateway swap --help
-usage: gateway swap [-h] [connector] [args ...]
-
-positional arguments:
- connector Connector name/type (e.g., jupiter/router)
- args Arguments: [base-quote] [side] [amount]
-
-options:
- -h, --help show this help message and exit
-```
-
-**Pattern 4:** usage: gateway list [-h]
-
-```
-usage: gateway list [-h]
-```
-
-**Pattern 5:** Example:
-
-```
-price = self.market_data_provider.get_price_by_type('binance', 'BTC-USDT', PriceType.MidPrice)
-```
-
-**Pattern 6:** Example:
-
-```
-price = self.market_data_provider.get_price_by_volume('binance', 'BTC-USDT', volume: 10000, True)
-```
-
-**Pattern 7:** Example:
-
-```
-price = self.market_data_provider.get_volume_for_price('binance', 'BTC-USDT', 70000, True)
-```
-
-**Pattern 8:** Example:
-
-```
-price = self.market_data_provider.get_order_book_snapshot('binance', 'BTC-USDT')
-```
-
-## Reference Files
-
-This skill includes comprehensive documentation in `references/`:
-
-- **advanced.md** - Advanced documentation
-- **configuration.md** - Configuration documentation
-- **connectors.md** - Connectors documentation
-- **development.md** - Development documentation
-- **getting_started.md** - Getting Started documentation
-- **other.md** - Other documentation
-- **strategies.md** - Strategies documentation
-- **trading.md** - Trading documentation
-- **troubleshooting.md** - Troubleshooting documentation
-
-Use `view` to read specific reference files when detailed information is needed.
-
-## Working with This Skill
-
-### For Beginners
-Start with the getting_started or tutorials reference files for foundational concepts.
-
-### For Specific Features
-Use the appropriate category reference file (api, guides, etc.) for detailed information.
-
-### For Code Examples
-The quick reference section above contains common patterns extracted from the official docs.
-
-## Resources
-
-### references/
-Organized documentation extracted from official sources. These files contain:
-- Detailed explanations
-- Code examples with language annotations
-- Links to original documentation
-- Table of contents for quick navigation
-
-### scripts/
-Add helper scripts here for common automation tasks.
-
-### assets/
-Add templates, boilerplate, or example projects here.
-
-## Notes
-
-- This skill was automatically generated from official documentation
-- Reference files preserve the structure and examples from source docs
-- Code examples include language detection for better syntax highlighting
-- Quick reference patterns are extracted from common usage examples in the docs
-
-## Updating
-
-To refresh this skill with updated documentation:
-1. Re-run the scraper with the same configuration
-2. The skill will be rebuilt with the latest information
diff --git a/assets/skills/markdown-to-epub/SKILL.md b/assets/skills/markdown-to-epub/SKILL.md
deleted file mode 100644
index 0d3e890..0000000
--- a/assets/skills/markdown-to-epub/SKILL.md
+++ /dev/null
@@ -1,92 +0,0 @@
----
-name: markdown-to-epub
-description: "将 Markdown 手稿与本地图片资产转换为可校验的 EPUB:修复/归一化图片引用与扩展名,保持标题层级 TOC,并做基础包结构检查。"
----
-
-# markdown-to-epub Skill
-
-把 Markdown 手稿(含本地图片)稳定构建为 EPUB:规范化图片引用、拷贝资产到可重复的构建目录、调用 Calibre `ebook-convert` 转换,并输出可核查报告。
-
-## When to Use This Skill
-
-触发条件(满足其一即可):
-- 需要把一份(或多份)Markdown 手稿打包交付为 EPUB。
-- 图片引用混乱(URL 编码、路径飘忽、扩展名不可信如 `.bin/.idunno`),需要自动归一化。
-- 需要在转换后做最基本的 EPUB 包结构检查(OPF/NCX/NAV、图片数量等)。
-
-## Not For / Boundaries
-
-- 不负责生成/改写正文内容(不会修改源手稿,只在构建目录里产出规范化版本)。
-- 不下载远程图片(`http(s)`/`data:` 引用会保持原样)。
-- 不替代真正的排版/校对流程(这里只做可交付构建与结构验证)。
-
-## Quick Start
-
-从仓库根目录执行(推荐 `python3`):
-
-```bash
-python3 assets/skills/markdown-to-epub/scripts/build_epub.py \
- --input-md "./book.md" \
- --output-epub "./book.epub" \
- --title "Book Title" \
- --authors "Author Name" \
- --language "zh-CN"
-```
-
-脚本会创建构建工作区(默认 `build_epub/`),包含:
-- `book.normalized.md`
-- `assets/`:拷贝后的图片(会按真实文件签名推断扩展名)
-- `conversion.log`
-- `report.json`
-
-## 依赖
-
-- 需要安装 Calibre,并确保 `ebook-convert` 在 `PATH` 中(或用 `--ebook-convert-bin` 指定路径)。
-
-## Missing Asset Recovery
-
-如果 Markdown 里引用了图片但文件找不到,可以提供一个 JSON 映射表(按「basename」匹配):
-
-```json
-{
- "missing-file.idunno": "replacement-file.idunno"
-}
-```
-
-然后重跑(示例):
-
-```bash
-python3 assets/skills/markdown-to-epub/scripts/build_epub.py \
- --input-md "./book.md" \
- --output-epub "./book.epub" \
- --fallback-map "./fallback-map.json"
-```
-
-## Operational Rules
-
-- 优先使用 `ebook-convert`;缺失时明确报错并快速失败。
-- 源手稿只读;所有输出写入 `build_dir/`。
-- TOC 以标题层级(`h1/h2/h3`)为准。
-- 缺失资产必须显式报告;严格模式下不允许静默跳过。
-- 命令保持非交互式。
-
-## Script Interface
-
-`scripts/build_epub.py` 参数:
-- `--input-md`(必选):源 Markdown 路径
-- `--output-epub`(可选):输出 EPUB 路径,默认 `.epub`
-- `--source-root`(可选):解析图片引用的根目录,默认使用 Markdown 所在目录
-- `--build-dir`(可选):构建工作区目录,默认 `/build_epub`
-- `--fallback-map`(可选):JSON 映射(缺失图片 basename → 替换 basename)
-- `--title` / `--authors` / `--language`:传给 `ebook-convert` 的元数据
-- `--input-encoding`:输入 Markdown 编码,默认 `utf-8`
-- `--strict-missing`:严格模式(有任何本地图片无法解析则失败,默认开启)
-- `--no-strict-missing`:关闭严格模式(保留未解析链接,继续转换)
-- `--ebook-convert-bin`:`ebook-convert` 可执行文件名/路径,默认 `ebook-convert`
-
-## Validation Checklist
-
-- 确认 EPUB 文件生成且大小不是「几 KB 的空壳」。
-- 确认 EPUB(zip)内包含 OPF 与 NCX/NAV。
-- 确认 EPUB 内图片数量不低于对手稿的预期。
-- 严格模式下确认 `report.json` 的 `missing_images` 为空。
diff --git a/assets/skills/polymarket/SKILL.md b/assets/skills/polymarket/SKILL.md
deleted file mode 100644
index 9cdd261..0000000
--- a/assets/skills/polymarket/SKILL.md
+++ /dev/null
@@ -1,233 +0,0 @@
----
-name: polymarket
-description: Comprehensive Polymarket skill covering prediction markets, API, trading, market data, and real-time WebSocket data streaming. Build applications with Polymarket services, monitor live trades, and integrate market predictions.
----
-
-# Polymarket Comprehensive Skill
-
-Complete assistance with Polymarket development - covering the full platform (API, trading, market data) and the real-time data streaming client (WebSocket subscriptions for live market activity).
-
-## When to Use This Skill
-
-This skill should be triggered when:
-
-**Platform & API:**
-- Working with Polymarket prediction markets
-- Using Polymarket API for market data
-- Implementing trading strategies
-- Building applications with Polymarket services
-- Learning Polymarket best practices
-
-**Real-Time Data Streaming:**
-- Connecting to Polymarket's WebSocket service
-- Building prediction market monitoring tools
-- Processing live trades, orders, and market updates
-- Monitoring market comments and social reactions
-- Tracking RFQ (Request for Quote) activity
-- Integrating crypto price feeds
-
-## Quick Reference
-
-### Real-Time Data Client Setup
-
-**Installation:**
-```bash
-npm install @polymarket/real-time-data-client
-```
-
-**Basic Usage:**
-```typescript
-import { RealTimeDataClient } from "@polymarket/real-time-data-client";
-
-const onMessage = (message: Message): void => {
- console.log(message.topic, message.type, message.payload);
-};
-
-const onConnect = (client: RealTimeDataClient): void => {
- client.subscribe({
- subscriptions: [{
- topic: "activity",
- type: "trades"
- }]
- });
-};
-
-new RealTimeDataClient({ onMessage, onConnect }).connect();
-```
-
-### Supported WebSocket Topics
-
-**1. Activity (`activity`)**
-- `trades` - Completed trades
-- `orders_matched` - Order matching events
-- Filters: `{"event_slug":"string"}` OR `{"market_slug":"string"}`
-
-**2. Comments (`comments`)**
-- `comment_created`, `comment_removed`
-- `reaction_created`, `reaction_removed`
-- Filters: `{"parentEntityID":number,"parentEntityType":"Event"}`
-
-**3. RFQ (`rfq`)**
-- Request/Quote lifecycle events
-- No filters, no auth required
-
-**4. Crypto Prices (`crypto_prices`, `crypto_prices_chainlink`)**
-- `update` - Real-time price feeds
-- Filters: `{"symbol":"BTC"}` (optional)
-
-**5. CLOB User (`clob_user`)** ⚠️ Requires Auth
-- `order` - User's order updates
-- `trade` - User's trade executions
-
-**6. CLOB Market (`clob_market`)**
-- `price_change` - Price movements
-- `agg_orderbook` - Aggregated order book
-- `last_trade_price` - Latest prices
-- `market_created`, `market_resolved`
-
-### Authentication for User Data
-
-```typescript
-client.subscribe({
- subscriptions: [{
- topic: "clob_user",
- type: "*",
- clob_auth: {
- key: "your-api-key",
- secret: "your-api-secret",
- passphrase: "your-passphrase"
- }
- }]
-});
-```
-
-### Common Use Cases
-
-**Monitor Specific Market:**
-```typescript
-client.subscribe({
- subscriptions: [{
- topic: "activity",
- type: "trades",
- filters: `{"market_slug":"btc-above-100k-2024"}`
- }]
-});
-```
-
-**Track Multiple Markets:**
-```typescript
-client.subscribe({
- subscriptions: [{
- topic: "clob_market",
- type: "price_change",
- filters: `["100","101","102"]`
- }]
-});
-```
-
-**Monitor Event Comments:**
-```typescript
-client.subscribe({
- subscriptions: [{
- topic: "comments",
- type: "*",
- filters: `{"parentEntityID":12345,"parentEntityType":"Event"}`
- }]
-});
-```
-
-## Reference Files
-
-This skill includes comprehensive documentation in `references/`:
-
-**Platform Documentation:**
-- **api.md** - Polymarket API documentation
-- **getting_started.md** - Getting started guide
-- **guides.md** - Development guides
-- **learn.md** - Learning resources
-- **trading.md** - Trading documentation
-- **other.md** - Additional resources
-
-**Real-Time Client:**
-- **README.md** - WebSocket client API and examples
-- **llms.md** - LLM integration guide
-- **llms-full.md** - Complete LLM documentation
-
-Use `view` to read specific reference files for detailed information.
-
-## Key Features
-
-**Platform Capabilities:**
-✅ Prediction market creation and resolution
-✅ Trading API (REST & WebSocket)
-✅ Market data queries
-✅ User portfolio management
-✅ Event and market discovery
-
-**Real-Time Streaming:**
-✅ WebSocket-based persistent connections
-✅ Topic-based subscriptions
-✅ Dynamic subscription management
-✅ Filter support for targeted data
-✅ User authentication for private data
-✅ TypeScript with full type safety
-✅ Initial data dumps on connection
-
-## Best Practices
-
-### WebSocket Connection Management
-- Use `onConnect` callback for subscriptions
-- Implement reconnection logic for production
-- Clean up with `disconnect()` when done
-- Handle authentication errors gracefully
-
-### Subscription Strategy
-- Use wildcards (`"*"`) sparingly
-- Apply filters to reduce data volume
-- Unsubscribe from unused streams
-- Process messages asynchronously
-
-### Performance
-- Consider batching high-frequency data
-- Use filters to minimize client processing
-- Validate message payloads before use
-
-## Requirements
-
-- **Node.js**: 14+ recommended
-- **TypeScript**: Optional but recommended
-- **Package Manager**: npm or yarn
-
-## Resources
-
-### Official Links
-- **Polymarket Platform**: https://polymarket.com
-- **Real-Time Client Repo**: https://github.com/Polymarket/real-time-data-client
-- **API Documentation**: See references/api.md
-
-### Working with This Skill
-
-**For Beginners:**
-Start with `getting_started.md` for foundational concepts.
-
-**For API Integration:**
-Use `api.md` and `trading.md` for REST API details.
-
-**For Real-Time Data:**
-Use `README.md` for WebSocket client implementation.
-
-**For LLM Integration:**
-Use `llms.md` and `llms-full.md` for AI/ML use cases.
-
-## Notes
-
-- Real-Time Client is TypeScript/JavaScript (not Python)
-- Some WebSocket topics require authentication
-- Use filters to manage message volume effectively
-- All timestamps are Unix timestamps
-- Market IDs are strings (e.g., "100", "101")
-- Platform documentation covers both REST API and WebSocket usage
-
----
-
-**This comprehensive skill combines Polymarket platform expertise with real-time data streaming capabilities!**
diff --git a/assets/skills/postgresql/SKILL.md b/assets/skills/postgresql/SKILL.md
deleted file mode 100644
index e699a0f..0000000
--- a/assets/skills/postgresql/SKILL.md
+++ /dev/null
@@ -1,143 +0,0 @@
----
-name: postgresql
-description: PostgreSQL database documentation - SQL queries, database design, administration, performance tuning, and advanced features. Use when working with PostgreSQL databases, writing SQL, or managing database systems.
----
-
-# Postgresql Skill
-
-Comprehensive assistance with postgresql development, generated from official documentation.
-
-## When to Use This Skill
-
-This skill should be triggered when:
-- Working with postgresql
-- Asking about postgresql features or APIs
-- Implementing postgresql solutions
-- Debugging postgresql code
-- Learning postgresql best practices
-
-## Quick Reference
-
-### Common Patterns
-
-**Pattern 1:** 32.1. Database Connection Control Functions # 32.1.1. Connection Strings 32.1.2. Parameter Key Words The following functions deal with making a connection to a PostgreSQL backend server. An application program can have several backend connections open at one time. (One reason to do that is to access more than one database.) Each connection is represented by a PGconn object, which is obtained from the function PQconnectdb, PQconnectdbParams, or PQsetdbLogin. Note that these functions will always return a non-null object pointer, unless perhaps there is too little memory even to allocate the PGconn object. The PQstatus function should be called to check the return value for a successful connection before queries are sent via the connection object. Warning If untrusted users have access to a database that has not adopted a secure schema usage pattern, begin each session by removing publicly-writable schemas from search_path. One can set parameter key word options to value -csearch_path=. Alternately, one can issue PQexec(conn, "SELECT pg_catalog.set_config('search_path', '', false)") after connecting. This consideration is not specific to libpq; it applies to every interface for executing arbitrary SQL commands. Warning On Unix, forking a process with open libpq connections can lead to unpredictable results because the parent and child processes share the same sockets and operating system resources. For this reason, such usage is not recommended, though doing an exec from the child process to load a new executable is safe. PQconnectdbParams # Makes a new connection to the database server. PGconn *PQconnectdbParams(const char * const *keywords, const char * const *values, int expand_dbname); This function opens a new database connection using the parameters taken from two NULL-terminated arrays. The first, keywords, is defined as an array of strings, each one being a key word. The second, values, gives the value for each key word. Unlike PQsetdbLogin below, the parameter set can be extended without changing the function signature, so use of this function (or its nonblocking analogs PQconnectStartParams and PQconnectPoll) is preferred for new application programming. The currently recognized parameter key words are listed in Section 32.1.2. The passed arrays can be empty to use all default parameters, or can contain one or more parameter settings. They must be matched in length. Processing will stop at the first NULL entry in the keywords array. Also, if the values entry associated with a non-NULL keywords entry is NULL or an empty string, that entry is ignored and processing continues with the next pair of array entries. When expand_dbname is non-zero, the value for the first dbname key word is checked to see if it is a connection string. If so, it is “expanded” into the individual connection parameters extracted from the string. The value is considered to be a connection string, rather than just a database name, if it contains an equal sign (=) or it begins with a URI scheme designator. (More details on connection string formats appear in Section 32.1.1.) Only the first occurrence of dbname is treated in this way; any subsequent dbname parameter is processed as a plain database name. In general the parameter arrays are processed from start to end. If any key word is repeated, the last value (that is not NULL or empty) is used. This rule applies in particular when a key word found in a connection string conflicts with one appearing in the keywords array. Thus, the programmer may determine whether array entries can override or be overridden by values taken from a connection string. Array entries appearing before an expanded dbname entry can be overridden by fields of the connection string, and in turn those fields are overridden by array entries appearing after dbname (but, again, only if those entries supply non-empty values). After processing all the array entries and any expanded connection string, any connection parameters that remain unset are filled with default values. If an unset parameter's corresponding environment variable (see Section 32.15) is set, its value is used. If the environment variable is not set either, then the parameter's built-in default value is used. PQconnectdb # Makes a new connection to the database server. PGconn *PQconnectdb(const char *conninfo); This function opens a new database connection using the parameters taken from the string conninfo. The passed string can be empty to use all default parameters, or it can contain one or more parameter settings separated by whitespace, or it can contain a URI. See Section 32.1.1 for details. PQsetdbLogin # Makes a new connection to the database server. PGconn *PQsetdbLogin(const char *pghost, const char *pgport, const char *pgoptions, const char *pgtty, const char *dbName, const char *login, const char *pwd); This is the predecessor of PQconnectdb with a fixed set of parameters. It has the same functionality except that the missing parameters will always take on default values. Write NULL or an empty string for any one of the fixed parameters that is to be defaulted. If the dbName contains an = sign or has a valid connection URI prefix, it is taken as a conninfo string in exactly the same way as if it had been passed to PQconnectdb, and the remaining parameters are then applied as specified for PQconnectdbParams. pgtty is no longer used and any value passed will be ignored. PQsetdb # Makes a new connection to the database server. PGconn *PQsetdb(char *pghost, char *pgport, char *pgoptions, char *pgtty, char *dbName); This is a macro that calls PQsetdbLogin with null pointers for the login and pwd parameters. It is provided for backward compatibility with very old programs. PQconnectStartParamsPQconnectStartPQconnectPoll # Make a connection to the database server in a nonblocking manner. PGconn *PQconnectStartParams(const char * const *keywords, const char * const *values, int expand_dbname); PGconn *PQconnectStart(const char *conninfo); PostgresPollingStatusType PQconnectPoll(PGconn *conn); These three functions are used to open a connection to a database server such that your application's thread of execution is not blocked on remote I/O whilst doing so. The point of this approach is that the waits for I/O to complete can occur in the application's main loop, rather than down inside PQconnectdbParams or PQconnectdb, and so the application can manage this operation in parallel with other activities. With PQconnectStartParams, the database connection is made using the parameters taken from the keywords and values arrays, and controlled by expand_dbname, as described above for PQconnectdbParams. With PQconnectStart, the database connection is made using the parameters taken from the string conninfo as described above for PQconnectdb. Neither PQconnectStartParams nor PQconnectStart nor PQconnectPoll will block, so long as a number of restrictions are met: The hostaddr parameter must be used appropriately to prevent DNS queries from being made. See the documentation of this parameter in Section 32.1.2 for details. If you call PQtrace, ensure that the stream object into which you trace will not block. You must ensure that the socket is in the appropriate state before calling PQconnectPoll, as described below. To begin a nonblocking connection request, call PQconnectStart or PQconnectStartParams. If the result is null, then libpq has been unable to allocate a new PGconn structure. Otherwise, a valid PGconn pointer is returned (though not yet representing a valid connection to the database). Next call PQstatus(conn). If the result is CONNECTION_BAD, the connection attempt has already failed, typically because of invalid connection parameters. If PQconnectStart or PQconnectStartParams succeeds, the next stage is to poll libpq so that it can proceed with the connection sequence. Use PQsocket(conn) to obtain the descriptor of the socket underlying the database connection. (Caution: do not assume that the socket remains the same across PQconnectPoll calls.) Loop thus: If PQconnectPoll(conn) last returned PGRES_POLLING_READING, wait until the socket is ready to read (as indicated by select(), poll(), or similar system function). Note that PQsocketPoll can help reduce boilerplate by abstracting the setup of select(2) or poll(2) if it is available on your system. Then call PQconnectPoll(conn) again. Conversely, if PQconnectPoll(conn) last returned PGRES_POLLING_WRITING, wait until the socket is ready to write, then call PQconnectPoll(conn) again. On the first iteration, i.e., if you have yet to call PQconnectPoll, behave as if it last returned PGRES_POLLING_WRITING. Continue this loop until PQconnectPoll(conn) returns PGRES_POLLING_FAILED, indicating the connection procedure has failed, or PGRES_POLLING_OK, indicating the connection has been successfully made. At any time during connection, the status of the connection can be checked by calling PQstatus. If this call returns CONNECTION_BAD, then the connection procedure has failed; if the call returns CONNECTION_OK, then the connection is ready. Both of these states are equally detectable from the return value of PQconnectPoll, described above. Other states might also occur during (and only during) an asynchronous connection procedure. These indicate the current stage of the connection procedure and might be useful to provide feedback to the user for example. These statuses are: CONNECTION_STARTED # Waiting for connection to be made. CONNECTION_MADE # Connection OK; waiting to send. CONNECTION_AWAITING_RESPONSE # Waiting for a response from the server. CONNECTION_AUTH_OK # Received authentication; waiting for backend start-up to finish. CONNECTION_SSL_STARTUP # Negotiating SSL encryption. CONNECTION_GSS_STARTUP # Negotiating GSS encryption. CONNECTION_CHECK_WRITABLE # Checking if connection is able to handle write transactions. CONNECTION_CHECK_STANDBY # Checking if connection is to a server in standby mode. CONNECTION_CONSUME # Consuming any remaining response messages on connection. Note that, although these constants will remain (in order to maintain compatibility), an application should never rely upon these occurring in a particular order, or at all, or on the status always being one of these documented values. An application might do something like this: switch(PQstatus(conn)) { case CONNECTION_STARTED: feedback = "Connecting..."; break; case CONNECTION_MADE: feedback = "Connected to server..."; break; . . . default: feedback = "Connecting..."; } The connect_timeout connection parameter is ignored when using PQconnectPoll; it is the application's responsibility to decide whether an excessive amount of time has elapsed. Otherwise, PQconnectStart followed by a PQconnectPoll loop is equivalent to PQconnectdb. Note that when PQconnectStart or PQconnectStartParams returns a non-null pointer, you must call PQfinish when you are finished with it, in order to dispose of the structure and any associated memory blocks. This must be done even if the connection attempt fails or is abandoned. PQsocketPoll # Poll a connection's underlying socket descriptor retrieved with PQsocket. The primary use of this function is iterating through the connection sequence described in the documentation of PQconnectStartParams. typedef int64_t pg_usec_time_t; int PQsocketPoll(int sock, int forRead, int forWrite, pg_usec_time_t end_time); This function performs polling of a file descriptor, optionally with a timeout. If forRead is nonzero, the function will terminate when the socket is ready for reading. If forWrite is nonzero, the function will terminate when the socket is ready for writing. The timeout is specified by end_time, which is the time to stop waiting expressed as a number of microseconds since the Unix epoch (that is, time_t times 1 million). Timeout is infinite if end_time is -1. Timeout is immediate (no blocking) if end_time is 0 (or indeed, any time before now). Timeout values can be calculated conveniently by adding the desired number of microseconds to the result of PQgetCurrentTimeUSec. Note that the underlying system calls may have less than microsecond precision, so that the actual delay may be imprecise. The function returns a value greater than 0 if the specified condition is met, 0 if a timeout occurred, or -1 if an error occurred. The error can be retrieved by checking the errno(3) value. In the event both forRead and forWrite are zero, the function immediately returns a timeout indication. PQsocketPoll is implemented using either poll(2) or select(2), depending on platform. See POLLIN and POLLOUT from poll(2), or readfds and writefds from select(2), for more information. PQconndefaults # Returns the default connection options. PQconninfoOption *PQconndefaults(void); typedef struct { char *keyword; /* The keyword of the option */ char *envvar; /* Fallback environment variable name */ char *compiled; /* Fallback compiled in default value */ char *val; /* Option's current value, or NULL */ char *label; /* Label for field in connect dialog */ char *dispchar; /* Indicates how to display this field in a connect dialog. Values are: "" Display entered value as is "*" Password field - hide value "D" Debug option - don't show by default */ int dispsize; /* Field size in characters for dialog */ } PQconninfoOption; Returns a connection options array. This can be used to determine all possible PQconnectdb options and their current default values. The return value points to an array of PQconninfoOption structures, which ends with an entry having a null keyword pointer. The null pointer is returned if memory could not be allocated. Note that the current default values (val fields) will depend on environment variables and other context. A missing or invalid service file will be silently ignored. Callers must treat the connection options data as read-only. After processing the options array, free it by passing it to PQconninfoFree. If this is not done, a small amount of memory is leaked for each call to PQconndefaults. PQconninfo # Returns the connection options used by a live connection. PQconninfoOption *PQconninfo(PGconn *conn); Returns a connection options array. This can be used to determine all possible PQconnectdb options and the values that were used to connect to the server. The return value points to an array of PQconninfoOption structures, which ends with an entry having a null keyword pointer. All notes above for PQconndefaults also apply to the result of PQconninfo. PQconninfoParse # Returns parsed connection options from the provided connection string. PQconninfoOption *PQconninfoParse(const char *conninfo, char **errmsg); Parses a connection string and returns the resulting options as an array; or returns NULL if there is a problem with the connection string. This function can be used to extract the PQconnectdb options in the provided connection string. The return value points to an array of PQconninfoOption structures, which ends with an entry having a null keyword pointer. All legal options will be present in the result array, but the PQconninfoOption for any option not present in the connection string will have val set to NULL; default values are not inserted. If errmsg is not NULL, then *errmsg is set to NULL on success, else to a malloc'd error string explaining the problem. (It is also possible for *errmsg to be set to NULL and the function to return NULL; this indicates an out-of-memory condition.) After processing the options array, free it by passing it to PQconninfoFree. If this is not done, some memory is leaked for each call to PQconninfoParse. Conversely, if an error occurs and errmsg is not NULL, be sure to free the error string using PQfreemem. PQfinish # Closes the connection to the server. Also frees memory used by the PGconn object. void PQfinish(PGconn *conn); Note that even if the server connection attempt fails (as indicated by PQstatus), the application should call PQfinish to free the memory used by the PGconn object. The PGconn pointer must not be used again after PQfinish has been called. PQreset # Resets the communication channel to the server. void PQreset(PGconn *conn); This function will close the connection to the server and attempt to establish a new connection, using all the same parameters previously used. This might be useful for error recovery if a working connection is lost. PQresetStartPQresetPoll # Reset the communication channel to the server, in a nonblocking manner. int PQresetStart(PGconn *conn); PostgresPollingStatusType PQresetPoll(PGconn *conn); These functions will close the connection to the server and attempt to establish a new connection, using all the same parameters previously used. This can be useful for error recovery if a working connection is lost. They differ from PQreset (above) in that they act in a nonblocking manner. These functions suffer from the same restrictions as PQconnectStartParams, PQconnectStart and PQconnectPoll. To initiate a connection reset, call PQresetStart. If it returns 0, the reset has failed. If it returns 1, poll the reset using PQresetPoll in exactly the same way as you would create the connection using PQconnectPoll. PQpingParams # PQpingParams reports the status of the server. It accepts connection parameters identical to those of PQconnectdbParams, described above. It is not necessary to supply correct user name, password, or database name values to obtain the server status; however, if incorrect values are provided, the server will log a failed connection attempt. PGPing PQpingParams(const char * const *keywords, const char * const *values, int expand_dbname); The function returns one of the following values: PQPING_OK # The server is running and appears to be accepting connections. PQPING_REJECT # The server is running but is in a state that disallows connections (startup, shutdown, or crash recovery). PQPING_NO_RESPONSE # The server could not be contacted. This might indicate that the server is not running, or that there is something wrong with the given connection parameters (for example, wrong port number), or that there is a network connectivity problem (for example, a firewall blocking the connection request). PQPING_NO_ATTEMPT # No attempt was made to contact the server, because the supplied parameters were obviously incorrect or there was some client-side problem (for example, out of memory). PQping # PQping reports the status of the server. It accepts connection parameters identical to those of PQconnectdb, described above. It is not necessary to supply correct user name, password, or database name values to obtain the server status; however, if incorrect values are provided, the server will log a failed connection attempt. PGPing PQping(const char *conninfo); The return values are the same as for PQpingParams. PQsetSSLKeyPassHook_OpenSSL # PQsetSSLKeyPassHook_OpenSSL lets an application override libpq's default handling of encrypted client certificate key files using sslpassword or interactive prompting. void PQsetSSLKeyPassHook_OpenSSL(PQsslKeyPassHook_OpenSSL_type hook); The application passes a pointer to a callback function with signature: int callback_fn(char *buf, int size, PGconn *conn); which libpq will then call instead of its default PQdefaultSSLKeyPassHook_OpenSSL handler. The callback should determine the password for the key and copy it to result-buffer buf of size size. The string in buf must be null-terminated. The callback must return the length of the password stored in buf excluding the null terminator. On failure, the callback should set buf[0] = '\0' and return 0. See PQdefaultSSLKeyPassHook_OpenSSL in libpq's source code for an example. If the user specified an explicit key location, its path will be in conn->sslkey when the callback is invoked. This will be empty if the default key path is being used. For keys that are engine specifiers, it is up to engine implementations whether they use the OpenSSL password callback or define their own handling. The app callback may choose to delegate unhandled cases to PQdefaultSSLKeyPassHook_OpenSSL, or call it first and try something else if it returns 0, or completely override it. The callback must not escape normal flow control with exceptions, longjmp(...), etc. It must return normally. PQgetSSLKeyPassHook_OpenSSL # PQgetSSLKeyPassHook_OpenSSL returns the current client certificate key password hook, or NULL if none has been set. PQsslKeyPassHook_OpenSSL_type PQgetSSLKeyPassHook_OpenSSL(void); 32.1.1. Connection Strings # Several libpq functions parse a user-specified string to obtain connection parameters. There are two accepted formats for these strings: plain keyword/value strings and URIs. URIs generally follow RFC 3986, except that multi-host connection strings are allowed as further described below. 32.1.1.1. Keyword/Value Connection Strings # In the keyword/value format, each parameter setting is in the form keyword = value, with space(s) between settings. Spaces around a setting's equal sign are optional. To write an empty value, or a value containing spaces, surround it with single quotes, for example keyword = 'a value'. Single quotes and backslashes within a value must be escaped with a backslash, i.e., \' and \\. Example: host=localhost port=5432 dbname=mydb connect_timeout=10 The recognized parameter key words are listed in Section 32.1.2. 32.1.1.2. Connection URIs # The general form for a connection URI is: postgresql://[userspec@][hostspec][/dbname][?paramspec] where userspec is: user[:password] and hostspec is: [host][:port][,...] and paramspec is: name=value[&...] The URI scheme designator can be either postgresql:// or postgres://. Each of the remaining URI parts is optional. The following examples illustrate valid URI syntax: postgresql:// postgresql://localhost postgresql://localhost:5433 postgresql://localhost/mydb postgresql://user@localhost postgresql://user:secret@localhost postgresql://other@localhost/otherdb?connect_timeout=10&application_name=myapp postgresql://host1:123,host2:456/somedb?target_session_attrs=any&application_name=myapp Values that would normally appear in the hierarchical part of the URI can alternatively be given as named parameters. For example: postgresql:///mydb?host=localhost&port=5433 All named parameters must match key words listed in Section 32.1.2, except that for compatibility with JDBC connection URIs, instances of ssl=true are translated into sslmode=require. The connection URI needs to be encoded with percent-encoding if it includes symbols with special meaning in any of its parts. Here is an example where the equal sign (=) is replaced with %3D and the space character with %20: postgresql://user@localhost:5433/mydb?options=-c%20synchronous_commit%3Doff The host part may be either a host name or an IP address. To specify an IPv6 address, enclose it in square brackets: postgresql://[2001:db8::1234]/database The host part is interpreted as described for the parameter host. In particular, a Unix-domain socket connection is chosen if the host part is either empty or looks like an absolute path name, otherwise a TCP/IP connection is initiated. Note, however, that the slash is a reserved character in the hierarchical part of the URI. So, to specify a non-standard Unix-domain socket directory, either omit the host part of the URI and specify the host as a named parameter, or percent-encode the path in the host part of the URI: postgresql:///dbname?host=/var/lib/postgresql postgresql://%2Fvar%2Flib%2Fpostgresql/dbname It is possible to specify multiple host components, each with an optional port component, in a single URI. A URI of the form postgresql://host1:port1,host2:port2,host3:port3/ is equivalent to a connection string of the form host=host1,host2,host3 port=port1,port2,port3. As further described below, each host will be tried in turn until a connection is successfully established. 32.1.1.3. Specifying Multiple Hosts # It is possible to specify multiple hosts to connect to, so that they are tried in the given order. In the Keyword/Value format, the host, hostaddr, and port options accept comma-separated lists of values. The same number of elements must be given in each option that is specified, such that e.g., the first hostaddr corresponds to the first host name, the second hostaddr corresponds to the second host name, and so forth. As an exception, if only one port is specified, it applies to all the hosts. In the connection URI format, you can list multiple host:port pairs separated by commas in the host component of the URI. In either format, a single host name can translate to multiple network addresses. A common example of this is a host that has both an IPv4 and an IPv6 address. When multiple hosts are specified, or when a single host name is translated to multiple addresses, all the hosts and addresses will be tried in order, until one succeeds. If none of the hosts can be reached, the connection fails. If a connection is established successfully, but authentication fails, the remaining hosts in the list are not tried. If a password file is used, you can have different passwords for different hosts. All the other connection options are the same for every host in the list; it is not possible to e.g., specify different usernames for different hosts. 32.1.2. Parameter Key Words # The currently recognized parameter key words are: host # Name of host to connect to. If a host name looks like an absolute path name, it specifies Unix-domain communication rather than TCP/IP communication; the value is the name of the directory in which the socket file is stored. (On Unix, an absolute path name begins with a slash. On Windows, paths starting with drive letters are also recognized.) If the host name starts with @, it is taken as a Unix-domain socket in the abstract namespace (currently supported on Linux and Windows). The default behavior when host is not specified, or is empty, is to connect to a Unix-domain socket in /tmp (or whatever socket directory was specified when PostgreSQL was built). On Windows, the default is to connect to localhost. A comma-separated list of host names is also accepted, in which case each host name in the list is tried in order; an empty item in the list selects the default behavior as explained above. See Section 32.1.1.3 for details. hostaddr # Numeric IP address of host to connect to. This should be in the standard IPv4 address format, e.g., 172.28.40.9. If your machine supports IPv6, you can also use those addresses. TCP/IP communication is always used when a nonempty string is specified for this parameter. If this parameter is not specified, the value of host will be looked up to find the corresponding IP address — or, if host specifies an IP address, that value will be used directly. Using hostaddr allows the application to avoid a host name look-up, which might be important in applications with time constraints. However, a host name is required for GSSAPI or SSPI authentication methods, as well as for verify-full SSL certificate verification. The following rules are used: If host is specified without hostaddr, a host name lookup occurs. (When using PQconnectPoll, the lookup occurs when PQconnectPoll first considers this host name, and it may cause PQconnectPoll to block for a significant amount of time.) If hostaddr is specified without host, the value for hostaddr gives the server network address. The connection attempt will fail if the authentication method requires a host name. If both host and hostaddr are specified, the value for hostaddr gives the server network address. The value for host is ignored unless the authentication method requires it, in which case it will be used as the host name. Note that authentication is likely to fail if host is not the name of the server at network address hostaddr. Also, when both host and hostaddr are specified, host is used to identify the connection in a password file (see Section 32.16). A comma-separated list of hostaddr values is also accepted, in which case each host in the list is tried in order. An empty item in the list causes the corresponding host name to be used, or the default host name if that is empty as well. See Section 32.1.1.3 for details. Without either a host name or host address, libpq will connect using a local Unix-domain socket; or on Windows, it will attempt to connect to localhost. port # Port number to connect to at the server host, or socket file name extension for Unix-domain connections. If multiple hosts were given in the host or hostaddr parameters, this parameter may specify a comma-separated list of ports of the same length as the host list, or it may specify a single port number to be used for all hosts. An empty string, or an empty item in a comma-separated list, specifies the default port number established when PostgreSQL was built. dbname # The database name. Defaults to be the same as the user name. In certain contexts, the value is checked for extended formats; see Section 32.1.1 for more details on those. user # PostgreSQL user name to connect as. Defaults to be the same as the operating system name of the user running the application. password # Password to be used if the server demands password authentication. passfile # Specifies the name of the file used to store passwords (see Section 32.16). Defaults to ~/.pgpass, or %APPDATA%\postgresql\pgpass.conf on Microsoft Windows. (No error is reported if this file does not exist.) require_auth # Specifies the authentication method that the client requires from the server. If the server does not use the required method to authenticate the client, or if the authentication handshake is not fully completed by the server, the connection will fail. A comma-separated list of methods may also be provided, of which the server must use exactly one in order for the connection to succeed. By default, any authentication method is accepted, and the server is free to skip authentication altogether. Methods may be negated with the addition of a ! prefix, in which case the server must not attempt the listed method; any other method is accepted, and the server is free not to authenticate the client at all. If a comma-separated list is provided, the server may not attempt any of the listed negated methods. Negated and non-negated forms may not be combined in the same setting. As a final special case, the none method requires the server not to use an authentication challenge. (It may also be negated, to require some form of authentication.) The following methods may be specified: password The server must request plaintext password authentication. md5 The server must request MD5 hashed password authentication. Warning Support for MD5-encrypted passwords is deprecated and will be removed in a future release of PostgreSQL. Refer to Section 20.5 for details about migrating to another password type. gss The server must either request a Kerberos handshake via GSSAPI or establish a GSS-encrypted channel (see also gssencmode). sspi The server must request Windows SSPI authentication. scram-sha-256 The server must successfully complete a SCRAM-SHA-256 authentication exchange with the client. oauth The server must request an OAuth bearer token from the client. none The server must not prompt the client for an authentication exchange. (This does not prohibit client certificate authentication via TLS, nor GSS authentication via its encrypted transport.) channel_binding # This option controls the client's use of channel binding. A setting of require means that the connection must employ channel binding, prefer means that the client will choose channel binding if available, and disable prevents the use of channel binding. The default is prefer if PostgreSQL is compiled with SSL support; otherwise the default is disable. Channel binding is a method for the server to authenticate itself to the client. It is only supported over SSL connections with PostgreSQL 11 or later servers using the SCRAM authentication method. connect_timeout # Maximum time to wait while connecting, in seconds (write as a decimal integer, e.g., 10). Zero, negative, or not specified means wait indefinitely. This timeout applies separately to each host name or IP address. For example, if you specify two hosts and connect_timeout is 5, each host will time out if no connection is made within 5 seconds, so the total time spent waiting for a connection might be up to 10 seconds. client_encoding # This sets the client_encoding configuration parameter for this connection. In addition to the values accepted by the corresponding server option, you can use auto to determine the right encoding from the current locale in the client (LC_CTYPE environment variable on Unix systems). options # Specifies command-line options to send to the server at connection start. For example, setting this to -c geqo=off or --geqo=off sets the session's value of the geqo parameter to off. Spaces within this string are considered to separate command-line arguments, unless escaped with a backslash (\); write \\ to represent a literal backslash. For a detailed discussion of the available options, consult Chapter 19. application_name # Specifies a value for the application_name configuration parameter. fallback_application_name # Specifies a fallback value for the application_name configuration parameter. This value will be used if no value has been given for application_name via a connection parameter or the PGAPPNAME environment variable. Specifying a fallback name is useful in generic utility programs that wish to set a default application name but allow it to be overridden by the user. keepalives # Controls whether client-side TCP keepalives are used. The default value is 1, meaning on, but you can change this to 0, meaning off, if keepalives are not wanted. This parameter is ignored for connections made via a Unix-domain socket. keepalives_idle # Controls the number of seconds of inactivity after which TCP should send a keepalive message to the server. A value of zero uses the system default. This parameter is ignored for connections made via a Unix-domain socket, or if keepalives are disabled. It is only supported on systems where TCP_KEEPIDLE or an equivalent socket option is available, and on Windows; on other systems, it has no effect. keepalives_interval # Controls the number of seconds after which a TCP keepalive message that is not acknowledged by the server should be retransmitted. A value of zero uses the system default. This parameter is ignored for connections made via a Unix-domain socket, or if keepalives are disabled. It is only supported on systems where TCP_KEEPINTVL or an equivalent socket option is available, and on Windows; on other systems, it has no effect. keepalives_count # Controls the number of TCP keepalives that can be lost before the client's connection to the server is considered dead. A value of zero uses the system default. This parameter is ignored for connections made via a Unix-domain socket, or if keepalives are disabled. It is only supported on systems where TCP_KEEPCNT or an equivalent socket option is available; on other systems, it has no effect. tcp_user_timeout # Controls the number of milliseconds that transmitted data may remain unacknowledged before a connection is forcibly closed. A value of zero uses the system default. This parameter is ignored for connections made via a Unix-domain socket. It is only supported on systems where TCP_USER_TIMEOUT is available; on other systems, it has no effect. replication # This option determines whether the connection should use the replication protocol instead of the normal protocol. This is what PostgreSQL replication connections as well as tools such as pg_basebackup use internally, but it can also be used by third-party applications. For a description of the replication protocol, consult Section 54.4. The following values, which are case-insensitive, are supported: true, on, yes, 1 The connection goes into physical replication mode. database The connection goes into logical replication mode, connecting to the database specified in the dbname parameter. false, off, no, 0 The connection is a regular one, which is the default behavior. In physical or logical replication mode, only the simple query protocol can be used. gssencmode # This option determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server. There are three modes: disable only try a non-GSSAPI-encrypted connection prefer (default) if there are GSSAPI credentials present (i.e., in a credentials cache), first try a GSSAPI-encrypted connection; if that fails or there are no credentials, try a non-GSSAPI-encrypted connection. This is the default when PostgreSQL has been compiled with GSSAPI support. require only try a GSSAPI-encrypted connection gssencmode is ignored for Unix domain socket communication. If PostgreSQL is compiled without GSSAPI support, using the require option will cause an error, while prefer will be accepted but libpq will not actually attempt a GSSAPI-encrypted connection. sslmode # This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. There are six modes: disable only try a non-SSL connection allow first try a non-SSL connection; if that fails, try an SSL connection prefer (default) first try an SSL connection; if that fails, try a non-SSL connection require only try an SSL connection. If a root CA file is present, verify the certificate in the same way as if verify-ca was specified verify-ca only try an SSL connection, and verify that the server certificate is issued by a trusted certificate authority (CA) verify-full only try an SSL connection, verify that the server certificate is issued by a trusted CA and that the requested server host name matches that in the certificate See Section 32.19 for a detailed description of how these options work. sslmode is ignored for Unix domain socket communication. If PostgreSQL is compiled without SSL support, using options require, verify-ca, or verify-full will cause an error, while options allow and prefer will be accepted but libpq will not actually attempt an SSL connection. Note that if GSSAPI encryption is possible, that will be used in preference to SSL encryption, regardless of the value of sslmode. To force use of SSL encryption in an environment that has working GSSAPI infrastructure (such as a Kerberos server), also set gssencmode to disable. requiressl # This option is deprecated in favor of the sslmode setting. If set to 1, an SSL connection to the server is required (this is equivalent to sslmode require). libpq will then refuse to connect if the server does not accept an SSL connection. If set to 0 (default), libpq will negotiate the connection type with the server (equivalent to sslmode prefer). This option is only available if PostgreSQL is compiled with SSL support. sslnegotiation # This option controls how SSL encryption is negotiated with the server, if SSL is used. In the default postgres mode, the client first asks the server if SSL is supported. In direct mode, the client starts the standard SSL handshake directly after establishing the TCP/IP connection. Traditional PostgreSQL protocol negotiation is the most flexible with different server configurations. If the server is known to support direct SSL connections then the latter requires one fewer round trip reducing connection latency and also allows the use of protocol agnostic SSL network tools. The direct SSL option was introduced in PostgreSQL version 17. postgres perform PostgreSQL protocol negotiation. This is the default if the option is not provided. direct start SSL handshake directly after establishing the TCP/IP connection. This is only allowed with sslmode=require or higher, because the weaker settings could lead to unintended fallback to plaintext authentication when the server does not support direct SSL handshake. sslcompression # If set to 1, data sent over SSL connections will be compressed. If set to 0, compression will be disabled. The default is 0. This parameter is ignored if a connection without SSL is made. SSL compression is nowadays considered insecure and its use is no longer recommended. OpenSSL 1.1.0 disabled compression by default, and many operating system distributions disabled it in prior versions as well, so setting this parameter to on will not have any effect if the server does not accept compression. PostgreSQL 14 disabled compression completely in the backend. If security is not a primary concern, compression can improve throughput if the network is the bottleneck. Disabling compression can improve response time and throughput if CPU performance is the limiting factor. sslcert # This parameter specifies the file name of the client SSL certificate, replacing the default ~/.postgresql/postgresql.crt. This parameter is ignored if an SSL connection is not made. sslkey # This parameter specifies the location for the secret key used for the client certificate. It can either specify a file name that will be used instead of the default ~/.postgresql/postgresql.key, or it can specify a key obtained from an external “engine” (engines are OpenSSL loadable modules). An external engine specification should consist of a colon-separated engine name and an engine-specific key identifier. This parameter is ignored if an SSL connection is not made. sslkeylogfile # This parameter specifies the location where libpq will log keys used in this SSL context. This is useful for debugging PostgreSQL protocol interactions or client connections using network inspection tools like Wireshark. This parameter is ignored if an SSL connection is not made, or if LibreSSL is used (LibreSSL does not support key logging). Keys are logged using the NSS format. Warning Key logging will expose potentially sensitive information in the keylog file. Keylog files should be handled with the same care as sslkey files. sslpassword # This parameter specifies the password for the secret key specified in sslkey, allowing client certificate private keys to be stored in encrypted form on disk even when interactive passphrase input is not practical. Specifying this parameter with any non-empty value suppresses the Enter PEM pass phrase: prompt that OpenSSL will emit by default when an encrypted client certificate key is provided to libpq. If the key is not encrypted this parameter is ignored. The parameter has no effect on keys specified by OpenSSL engines unless the engine uses the OpenSSL password callback mechanism for prompts. There is no environment variable equivalent to this option, and no facility for looking it up in .pgpass. It can be used in a service file connection definition. Users with more sophisticated uses should consider using OpenSSL engines and tools like PKCS#11 or USB crypto offload devices. sslcertmode # This option determines whether a client certificate may be sent to the server, and whether the server is required to request one. There are three modes: disable A client certificate is never sent, even if one is available (default location or provided via sslcert). allow (default) A certificate may be sent, if the server requests one and the client has one to send. require The server must request a certificate. The connection will fail if the client does not send a certificate and the server successfully authenticates the client anyway. Note sslcertmode=require doesn't add any additional security, since there is no guarantee that the server is validating the certificate correctly; PostgreSQL servers generally request TLS certificates from clients whether they validate them or not. The option may be useful when troubleshooting more complicated TLS setups. sslrootcert # This parameter specifies the name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities. The default is ~/.postgresql/root.crt. The special value system may be specified instead, in which case the trusted CA roots from the SSL implementation will be loaded. The exact locations of these root certificates differ by SSL implementation and platform. For OpenSSL in particular, the locations may be further modified by the SSL_CERT_DIR and SSL_CERT_FILE environment variables. Note When using sslrootcert=system, the default sslmode is changed to verify-full, and any weaker setting will result in an error. In most cases it is trivial for anyone to obtain a certificate trusted by the system for a hostname they control, rendering verify-ca and all weaker modes useless. The magic system value will take precedence over a local certificate file with the same name. If for some reason you find yourself in this situation, use an alternative path like sslrootcert=./system instead. sslcrl # This parameter specifies the file name of the SSL server certificate revocation list (CRL). Certificates listed in this file, if it exists, will be rejected while attempting to authenticate the server's certificate. If neither sslcrl nor sslcrldir is set, this setting is taken as ~/.postgresql/root.crl. sslcrldir # This parameter specifies the directory name of the SSL server certificate revocation list (CRL). Certificates listed in the files in this directory, if it exists, will be rejected while attempting to authenticate the server's certificate. The directory needs to be prepared with the OpenSSL command openssl rehash or c_rehash. See its documentation for details. Both sslcrl and sslcrldir can be specified together. sslsni # If set to 1 (default), libpq sets the TLS extension “Server Name Indication” (SNI) on SSL-enabled connections. By setting this parameter to 0, this is turned off. The Server Name Indication can be used by SSL-aware proxies to route connections without having to decrypt the SSL stream. (Note that unless the proxy is aware of the PostgreSQL protocol handshake this would require setting sslnegotiation to direct.) However, SNI makes the destination host name appear in cleartext in the network traffic, so it might be undesirable in some cases. requirepeer # This parameter specifies the operating-system user name of the server, for example requirepeer=postgres. When making a Unix-domain socket connection, if this parameter is set, the client checks at the beginning of the connection that the server process is running under the specified user name; if it is not, the connection is aborted with an error. This parameter can be used to provide server authentication similar to that available with SSL certificates on TCP/IP connections. (Note that if the Unix-domain socket is in /tmp or another publicly writable location, any user could start a server listening there. Use this parameter to ensure that you are connected to a server run by a trusted user.) This option is only supported on platforms for which the peer authentication method is implemented; see Section 20.9. ssl_min_protocol_version # This parameter specifies the minimum SSL/TLS protocol version to allow for the connection. Valid values are TLSv1, TLSv1.1, TLSv1.2 and TLSv1.3. The supported protocols depend on the version of OpenSSL used, older versions not supporting the most modern protocol versions. If not specified, the default is TLSv1.2, which satisfies industry best practices as of this writing. ssl_max_protocol_version # This parameter specifies the maximum SSL/TLS protocol version to allow for the connection. Valid values are TLSv1, TLSv1.1, TLSv1.2 and TLSv1.3. The supported protocols depend on the version of OpenSSL used, older versions not supporting the most modern protocol versions. If not set, this parameter is ignored and the connection will use the maximum bound defined by the backend, if set. Setting the maximum protocol version is mainly useful for testing or if some component has issues working with a newer protocol. min_protocol_version # Specifies the minimum protocol version to allow for the connection. The default is to allow any version of the PostgreSQL protocol supported by libpq, which currently means 3.0. If the server does not support at least this protocol version the connection will be closed. The current supported values are 3.0, 3.2, and latest. The latest value is equivalent to the latest protocol version supported by the libpq version being used, which is currently 3.2. max_protocol_version # Specifies the protocol version to request from the server. The default is to use version 3.0 of the PostgreSQL protocol, unless the connection string specifies a feature that relies on a higher protocol version, in which case the latest version supported by libpq is used. If the server does not support the protocol version requested by the client, the connection is automatically downgraded to a lower minor protocol version that the server supports. After the connection attempt has completed you can use PQprotocolVersion to find out which exact protocol version was negotiated. The current supported values are 3.0, 3.2, and latest. The latest value is equivalent to the latest protocol version supported by the libpq version being used, which is currently 3.2. krbsrvname # Kerberos service name to use when authenticating with GSSAPI. This must match the service name specified in the server configuration for Kerberos authentication to succeed. (See also Section 20.6.) The default value is normally postgres, but that can be changed when building PostgreSQL via the --with-krb-srvnam option of configure. In most environments, this parameter never needs to be changed. Some Kerberos implementations might require a different service name, such as Microsoft Active Directory which requires the service name to be in upper case (POSTGRES). gsslib # GSS library to use for GSSAPI authentication. Currently this is disregarded except on Windows builds that include both GSSAPI and SSPI support. In that case, set this to gssapi to cause libpq to use the GSSAPI library for authentication instead of the default SSPI. gssdelegation # Forward (delegate) GSS credentials to the server. The default is 0 which means credentials will not be forwarded to the server. Set this to 1 to have credentials forwarded when possible. scram_client_key # The base64-encoded SCRAM client key. This can be used by foreign-data wrappers or similar middleware to enable pass-through SCRAM authentication. See Section F.38.1.10 for one such implementation. It is not meant to be specified directly by users or client applications. scram_server_key # The base64-encoded SCRAM server key. This can be used by foreign-data wrappers or similar middleware to enable pass-through SCRAM authentication. See Section F.38.1.10 for one such implementation. It is not meant to be specified directly by users or client applications. service # Service name to use for additional parameters. It specifies a service name in pg_service.conf that holds additional connection parameters. This allows applications to specify only a service name so connection parameters can be centrally maintained. See Section 32.17. target_session_attrs # This option determines whether the session must have certain properties to be acceptable. It's typically used in combination with multiple host names to select the first acceptable alternative among several hosts. There are six modes: any (default) any successful connection is acceptable read-write session must accept read-write transactions by default (that is, the server must not be in hot standby mode and the default_transaction_read_only parameter must be off) read-only session must not accept read-write transactions by default (the converse) primary server must not be in hot standby mode standby server must be in hot standby mode prefer-standby first try to find a standby server, but if none of the listed hosts is a standby server, try again in any mode load_balance_hosts # Controls the order in which the client tries to connect to the available hosts and addresses. Once a connection attempt is successful no other hosts and addresses will be tried. This parameter is typically used in combination with multiple host names or a DNS record that returns multiple IPs. This parameter can be used in combination with target_session_attrs to, for example, load balance over standby servers only. Once successfully connected, subsequent queries on the returned connection will all be sent to the same server. There are currently two modes: disable (default) No load balancing across hosts is performed. Hosts are tried in the order in which they are provided and addresses are tried in the order they are received from DNS or a hosts file. random Hosts and addresses are tried in random order. This value is mostly useful when opening multiple connections at the same time, possibly from different machines. This way connections can be load balanced across multiple PostgreSQL servers. While random load balancing, due to its random nature, will almost never result in a completely uniform distribution, it statistically gets quite close. One important aspect here is that this algorithm uses two levels of random choices: First the hosts will be resolved in random order. Then secondly, before resolving the next host, all resolved addresses for the current host will be tried in random order. This behaviour can skew the amount of connections each node gets greatly in certain cases, for instance when some hosts resolve to more addresses than others. But such a skew can also be used on purpose, e.g. to increase the number of connections a larger server gets by providing its hostname multiple times in the host string. When using this value it's recommended to also configure a reasonable value for connect_timeout. Because then, if one of the nodes that are used for load balancing is not responding, a new node will be tried. oauth_issuer # The HTTPS URL of a trusted issuer to contact if the server requests an OAuth token for the connection. This parameter is required for all OAuth connections; it should exactly match the issuer setting in the server's HBA configuration. As part of the standard authentication handshake, libpq will ask the server for a discovery document: a URL providing a set of OAuth configuration parameters. The server must provide a URL that is directly constructed from the components of the oauth_issuer, and this value must exactly match the issuer identifier that is declared in the discovery document itself, or the connection will fail. This is required to prevent a class of "mix-up attacks" on OAuth clients. You may also explicitly set oauth_issuer to the /.well-known/ URI used for OAuth discovery. In this case, if the server asks for a different URL, the connection will fail, but a custom OAuth flow may be able to speed up the standard handshake by using previously cached tokens. (In this case, it is recommended that oauth_scope be set as well, since the client will not have a chance to ask the server for a correct scope setting, and the default scopes for a token may not be sufficient to connect.) libpq currently supports the following well-known endpoints: /.well-known/openid-configuration /.well-known/oauth-authorization-server Warning Issuers are highly privileged during the OAuth connection handshake. As a rule of thumb, if you would not trust the operator of a URL to handle access to your servers, or to impersonate you directly, that URL should not be trusted as an oauth_issuer. oauth_client_id # An OAuth 2.0 client identifier, as issued by the authorization server. If the PostgreSQL server requests an OAuth token for the connection (and if no custom OAuth hook is installed to provide one), then this parameter must be set; otherwise, the connection will fail. oauth_client_secret # The client password, if any, to use when contacting the OAuth authorization server. Whether this parameter is required or not is determined by the OAuth provider; "public" clients generally do not use a secret, whereas "confidential" clients generally do. oauth_scope # The scope of the access request sent to the authorization server, specified as a (possibly empty) space-separated list of OAuth scope identifiers. This parameter is optional and intended for advanced usage. Usually the client will obtain appropriate scope settings from the PostgreSQL server. If this parameter is used, the server's requested scope list will be ignored. This can prevent a less-trusted server from requesting inappropriate access scopes from the end user. However, if the client's scope setting does not contain the server's required scopes, the server is likely to reject the issued token, and the connection will fail. The meaning of an empty scope list is provider-dependent. An OAuth authorization server may choose to issue a token with "default scope", whatever that happens to be, or it may reject the token request entirely.
-
-```
-PGconn
-```
-
-**Pattern 2:** 32.1.1. Connection Strings # Several libpq functions parse a user-specified string to obtain connection parameters. There are two accepted formats for these strings: plain keyword/value strings and URIs. URIs generally follow RFC 3986, except that multi-host connection strings are allowed as further described below. 32.1.1.1. Keyword/Value Connection Strings # In the keyword/value format, each parameter setting is in the form keyword = value, with space(s) between settings. Spaces around a setting's equal sign are optional. To write an empty value, or a value containing spaces, surround it with single quotes, for example keyword = 'a value'. Single quotes and backslashes within a value must be escaped with a backslash, i.e., \' and \\. Example: host=localhost port=5432 dbname=mydb connect_timeout=10 The recognized parameter key words are listed in Section 32.1.2. 32.1.1.2. Connection URIs # The general form for a connection URI is: postgresql://[userspec@][hostspec][/dbname][?paramspec] where userspec is: user[:password] and hostspec is: [host][:port][,...] and paramspec is: name=value[&...] The URI scheme designator can be either postgresql:// or postgres://. Each of the remaining URI parts is optional. The following examples illustrate valid URI syntax: postgresql:// postgresql://localhost postgresql://localhost:5433 postgresql://localhost/mydb postgresql://user@localhost postgresql://user:secret@localhost postgresql://other@localhost/otherdb?connect_timeout=10&application_name=myapp postgresql://host1:123,host2:456/somedb?target_session_attrs=any&application_name=myapp Values that would normally appear in the hierarchical part of the URI can alternatively be given as named parameters. For example: postgresql:///mydb?host=localhost&port=5433 All named parameters must match key words listed in Section 32.1.2, except that for compatibility with JDBC connection URIs, instances of ssl=true are translated into sslmode=require. The connection URI needs to be encoded with percent-encoding if it includes symbols with special meaning in any of its parts. Here is an example where the equal sign (=) is replaced with %3D and the space character with %20: postgresql://user@localhost:5433/mydb?options=-c%20synchronous_commit%3Doff The host part may be either a host name or an IP address. To specify an IPv6 address, enclose it in square brackets: postgresql://[2001:db8::1234]/database The host part is interpreted as described for the parameter host. In particular, a Unix-domain socket connection is chosen if the host part is either empty or looks like an absolute path name, otherwise a TCP/IP connection is initiated. Note, however, that the slash is a reserved character in the hierarchical part of the URI. So, to specify a non-standard Unix-domain socket directory, either omit the host part of the URI and specify the host as a named parameter, or percent-encode the path in the host part of the URI: postgresql:///dbname?host=/var/lib/postgresql postgresql://%2Fvar%2Flib%2Fpostgresql/dbname It is possible to specify multiple host components, each with an optional port component, in a single URI. A URI of the form postgresql://host1:port1,host2:port2,host3:port3/ is equivalent to a connection string of the form host=host1,host2,host3 port=port1,port2,port3. As further described below, each host will be tried in turn until a connection is successfully established. 32.1.1.3. Specifying Multiple Hosts # It is possible to specify multiple hosts to connect to, so that they are tried in the given order. In the Keyword/Value format, the host, hostaddr, and port options accept comma-separated lists of values. The same number of elements must be given in each option that is specified, such that e.g., the first hostaddr corresponds to the first host name, the second hostaddr corresponds to the second host name, and so forth. As an exception, if only one port is specified, it applies to all the hosts. In the connection URI format, you can list multiple host:port pairs separated by commas in the host component of the URI. In either format, a single host name can translate to multiple network addresses. A common example of this is a host that has both an IPv4 and an IPv6 address. When multiple hosts are specified, or when a single host name is translated to multiple addresses, all the hosts and addresses will be tried in order, until one succeeds. If none of the hosts can be reached, the connection fails. If a connection is established successfully, but authentication fails, the remaining hosts in the list are not tried. If a password file is used, you can have different passwords for different hosts. All the other connection options are the same for every host in the list; it is not possible to e.g., specify different usernames for different hosts.
-
-```
-keyword
-```
-
-**Pattern 3:** Example:
-
-```
-host=localhost port=5432 dbname=mydb connect_timeout=10
-```
-
-**Pattern 4:** 32.1.1.2. Connection URIs # The general form for a connection URI is: postgresql://[userspec@][hostspec][/dbname][?paramspec] where userspec is: user[:password] and hostspec is: [host][:port][,...] and paramspec is: name=value[&...] The URI scheme designator can be either postgresql:// or postgres://. Each of the remaining URI parts is optional. The following examples illustrate valid URI syntax: postgresql:// postgresql://localhost postgresql://localhost:5433 postgresql://localhost/mydb postgresql://user@localhost postgresql://user:secret@localhost postgresql://other@localhost/otherdb?connect_timeout=10&application_name=myapp postgresql://host1:123,host2:456/somedb?target_session_attrs=any&application_name=myapp Values that would normally appear in the hierarchical part of the URI can alternatively be given as named parameters. For example: postgresql:///mydb?host=localhost&port=5433 All named parameters must match key words listed in Section 32.1.2, except that for compatibility with JDBC connection URIs, instances of ssl=true are translated into sslmode=require. The connection URI needs to be encoded with percent-encoding if it includes symbols with special meaning in any of its parts. Here is an example where the equal sign (=) is replaced with %3D and the space character with %20: postgresql://user@localhost:5433/mydb?options=-c%20synchronous_commit%3Doff The host part may be either a host name or an IP address. To specify an IPv6 address, enclose it in square brackets: postgresql://[2001:db8::1234]/database The host part is interpreted as described for the parameter host. In particular, a Unix-domain socket connection is chosen if the host part is either empty or looks like an absolute path name, otherwise a TCP/IP connection is initiated. Note, however, that the slash is a reserved character in the hierarchical part of the URI. So, to specify a non-standard Unix-domain socket directory, either omit the host part of the URI and specify the host as a named parameter, or percent-encode the path in the host part of the URI: postgresql:///dbname?host=/var/lib/postgresql postgresql://%2Fvar%2Flib%2Fpostgresql/dbname It is possible to specify multiple host components, each with an optional port component, in a single URI. A URI of the form postgresql://host1:port1,host2:port2,host3:port3/ is equivalent to a connection string of the form host=host1,host2,host3 port=port1,port2,port3. As further described below, each host will be tried in turn until a connection is successfully established.
-
-```
-postgresql://[userspec@][hostspec][/dbname][?paramspec]
-
-where userspec is:
-
-user[:password]
-
-and hostspec is:
-
-[host][:port][,...]
-
-and paramspec is:
-
-name=value[&...]
-```
-
-**Pattern 5:** 21.5. Predefined Roles # PostgreSQL provides a set of predefined roles that provide access to certain, commonly needed, privileged capabilities and information. Administrators (including roles that have the CREATEROLE privilege) can GRANT these roles to users and/or other roles in their environment, providing those users with access to the specified capabilities and information. For example: GRANT pg_signal_backend TO admin_user; Warning Care should be taken when granting these roles to ensure they are only used where needed and with the understanding that these roles grant access to privileged information. The predefined roles are described below. Note that the specific permissions for each of the roles may change in the future as additional capabilities are added. Administrators should monitor the release notes for changes. pg_checkpoint # pg_checkpoint allows executing the CHECKPOINT command. pg_create_subscription # pg_create_subscription allows users with CREATE permission on the database to issue CREATE SUBSCRIPTION. pg_database_owner # pg_database_owner always has exactly one implicit member: the current database owner. It cannot be granted membership in any role, and no role can be granted membership in pg_database_owner. However, like any other role, it can own objects and receive grants of access privileges. Consequently, once pg_database_owner has rights within a template database, each owner of a database instantiated from that template will possess those rights. Initially, this role owns the public schema, so each database owner governs local use of that schema. pg_maintain # pg_maintain allows executing VACUUM, ANALYZE, CLUSTER, REFRESH MATERIALIZED VIEW, REINDEX, and LOCK TABLE on all relations, as if having MAINTAIN rights on those objects. pg_monitorpg_read_all_settingspg_read_all_statspg_stat_scan_tables # These roles are intended to allow administrators to easily configure a role for the purpose of monitoring the database server. They grant a set of common privileges allowing the role to read various useful configuration settings, statistics, and other system information normally restricted to superusers. pg_monitor allows reading/executing various monitoring views and functions. This role is a member of pg_read_all_settings, pg_read_all_stats and pg_stat_scan_tables. pg_read_all_settings allows reading all configuration variables, even those normally visible only to superusers. pg_read_all_stats allows reading all pg_stat_* views and use various statistics related extensions, even those normally visible only to superusers. pg_stat_scan_tables allows executing monitoring functions that may take ACCESS SHARE locks on tables, potentially for a long time (e.g., pgrowlocks(text) in the pgrowlocks extension). pg_read_all_datapg_write_all_data # pg_read_all_data allows reading all data (tables, views, sequences), as if having SELECT rights on those objects and USAGE rights on all schemas. This role does not bypass row-level security (RLS) policies. If RLS is being used, an administrator may wish to set BYPASSRLS on roles which this role is granted to. pg_write_all_data allows writing all data (tables, views, sequences), as if having INSERT, UPDATE, and DELETE rights on those objects and USAGE rights on all schemas. This role does not bypass row-level security (RLS) policies. If RLS is being used, an administrator may wish to set BYPASSRLS on roles which this role is granted to. pg_read_server_filespg_write_server_filespg_execute_server_program # These roles are intended to allow administrators to have trusted, but non-superuser, roles which are able to access files and run programs on the database server as the user the database runs as. They bypass all database-level permission checks when accessing files directly and they could be used to gain superuser-level access. Therefore, great care should be taken when granting these roles to users. pg_read_server_files allows reading files from any location the database can access on the server using COPY and other file-access functions. pg_write_server_files allows writing to files in any location the database can access on the server using COPY and other file-access functions. pg_execute_server_program allows executing programs on the database server as the user the database runs as using COPY and other functions which allow executing a server-side program. pg_signal_autovacuum_worker # pg_signal_autovacuum_worker allows signaling autovacuum workers to cancel the current table's vacuum or terminate its session. See Section 9.28.2. pg_signal_backend # pg_signal_backend allows signaling another backend to cancel a query or terminate its session. Note that this role does not permit signaling backends owned by a superuser. See Section 9.28.2. pg_use_reserved_connections # pg_use_reserved_connections allows use of connection slots reserved via reserved_connections.
-
-```
-CREATEROLE
-```
-
-**Pattern 6:** 6.4. Returning Data from Modified Rows # Sometimes it is useful to obtain data from modified rows while they are being manipulated. The INSERT, UPDATE, DELETE, and MERGE commands all have an optional RETURNING clause that supports this. Use of RETURNING avoids performing an extra database query to collect the data, and is especially valuable when it would otherwise be difficult to identify the modified rows reliably. The allowed contents of a RETURNING clause are the same as a SELECT command's output list (see Section 7.3). It can contain column names of the command's target table, or value expressions using those columns. A common shorthand is RETURNING *, which selects all columns of the target table in order. In an INSERT, the default data available to RETURNING is the row as it was inserted. This is not so useful in trivial inserts, since it would just repeat the data provided by the client. But it can be very handy when relying on computed default values. For example, when using a serial column to provide unique identifiers, RETURNING can return the ID assigned to a new row: CREATE TABLE users (firstname text, lastname text, id serial primary key); INSERT INTO users (firstname, lastname) VALUES ('Joe', 'Cool') RETURNING id; The RETURNING clause is also very useful with INSERT ... SELECT. In an UPDATE, the default data available to RETURNING is the new content of the modified row. For example: UPDATE products SET price = price * 1.10 WHERE price <= 99.99 RETURNING name, price AS new_price; In a DELETE, the default data available to RETURNING is the content of the deleted row. For example: DELETE FROM products WHERE obsoletion_date = 'today' RETURNING *; In a MERGE, the default data available to RETURNING is the content of the source row plus the content of the inserted, updated, or deleted target row. Since it is quite common for the source and target to have many of the same columns, specifying RETURNING * can lead to a lot of duplicated columns, so it is often more useful to qualify it so as to return just the source or target row. For example: MERGE INTO products p USING new_products n ON p.product_no = n.product_no WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price) WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price RETURNING p.*; In each of these commands, it is also possible to explicitly return the old and new content of the modified row. For example: UPDATE products SET price = price * 1.10 WHERE price <= 99.99 RETURNING name, old.price AS old_price, new.price AS new_price, new.price - old.price AS price_change; In this example, writing new.price is the same as just writing price, but it makes the meaning clearer. This syntax for returning old and new values is available in INSERT, UPDATE, DELETE, and MERGE commands, but typically old values will be NULL for an INSERT, and new values will be NULL for a DELETE. However, there are situations where it can still be useful for those commands. For example, in an INSERT with an ON CONFLICT DO UPDATE clause, the old values will be non-NULL for conflicting rows. Similarly, if a DELETE is turned into an UPDATE by a rewrite rule, the new values may be non-NULL. If there are triggers (Chapter 37) on the target table, the data available to RETURNING is the row as modified by the triggers. Thus, inspecting columns computed by triggers is another common use-case for RETURNING.
-
-```
-INSERT
-```
-
-**Pattern 7:** In an UPDATE, the default data available to RETURNING is the new content of the modified row. For example:
-
-```
-UPDATE
-```
-
-**Pattern 8:** In a DELETE, the default data available to RETURNING is the content of the deleted row. For example:
-
-```
-DELETE
-```
-
-### Example Code Patterns
-
-**Example 1** (javascript):
-```javascript
-PGconn *PQconnectdbParams(const char * const *keywords,
- const char * const *values,
- int expand_dbname);
-```
-
-**Example 2** (javascript):
-```javascript
-PGconn *PQconnectdb(const char *conninfo);
-```
-
-## Reference Files
-
-This skill includes comprehensive documentation in `references/`:
-
-- **getting_started.md** - Getting Started documentation
-- **sql.md** - Sql documentation
-
-Use `view` to read specific reference files when detailed information is needed.
-
-## Working with This Skill
-
-### For Beginners
-Start with the getting_started or tutorials reference files for foundational concepts.
-
-### For Specific Features
-Use the appropriate category reference file (api, guides, etc.) for detailed information.
-
-### For Code Examples
-The quick reference section above contains common patterns extracted from the official docs.
-
-## Resources
-
-### references/
-Organized documentation extracted from official sources. These files contain:
-- Detailed explanations
-- Code examples with language annotations
-- Links to original documentation
-- Table of contents for quick navigation
-
-### scripts/
-Add helper scripts here for common automation tasks.
-
-### assets/
-Add templates, boilerplate, or example projects here.
-
-## Notes
-
-- This skill was automatically generated from official documentation
-- Reference files preserve the structure and examples from source docs
-- Code examples include language detection for better syntax highlighting
-- Quick reference patterns are extracted from common usage examples in the docs
-
-## Updating
-
-To refresh this skill with updated documentation:
-1. Re-run the scraper with the same configuration
-2. The skill will be rebuilt with the latest information
diff --git a/assets/skills/proxychains/SKILL.md b/assets/skills/proxychains/SKILL.md
deleted file mode 100644
index 156c813..0000000
--- a/assets/skills/proxychains/SKILL.md
+++ /dev/null
@@ -1,291 +0,0 @@
----
-name: proxychains
-description: Auto-detect network issues and force proxy usage with proxychains4. Use this skill when encountering connection timeouts, DNS failures, or blocked network access. Default proxy is http://127.0.0.1:9910
----
-
-# Proxychains Network Proxy Skill
-
-**自动使用代理解决网络问题** - 当遇到网络连接失败、超时或访问受限时,自动使用 proxychains4 强制走代理。
-
-## When to Use This Skill
-
-**触发条件(自动应用):**
-- ❌ Connection timeout errors
-- ❌ DNS resolution failures
-- ❌ "Connection refused" errors
-- ❌ HTTP 403/451 blocked access
-- ❌ GitHub/PyPI/npm 等国外资源访问失败
-- ❌ curl/wget/git/pip 等命令失败
-
-**Claude 应该:**
-1. 检测到网络错误后,自动在命令前添加 `proxychains4`
-2. 使用默认代理配置:`http://127.0.0.1:9910`
-3. 如果代理失败,提示用户检查代理服务状态
-
-## Quick Reference
-
-### 基本用法模式
-
-```bash
-# ❌ 原命令失败
-curl https://github.com/user/repo
-
-# ✅ 使用代理重试
-proxychains4 curl https://github.com/user/repo
-```
-
-### 常见场景自动应用
-
-**场景 1: Git 操作失败**
-```bash
-# 原命令
-git clone https://github.com/user/repo.git
-
-# 自动改为
-proxychains4 git clone https://github.com/user/repo.git
-```
-
-**场景 2: Python pip 安装失败**
-```bash
-# 原命令
-pip install requests
-
-# 自动改为
-proxychains4 pip install requests
-```
-
-**场景 3: npm/yarn 安装失败**
-```bash
-# 原命令
-npm install package-name
-
-# 自动改为
-proxychains4 npm install package-name
-```
-
-**场景 4: wget/curl 下载失败**
-```bash
-# 原命令
-wget https://example.com/file.tar.gz
-
-# 自动改为
-proxychains4 wget https://example.com/file.tar.gz
-```
-
-**场景 5: Docker 拉取镜像失败**
-```bash
-# 原命令
-docker pull image:tag
-
-# 自动改为
-proxychains4 docker pull image:tag
-```
-
-**场景 6: SSH 连接失败**
-```bash
-# 原命令
-ssh user@remote-host
-
-# 自动改为
-proxychains4 ssh user@remote-host
-```
-
-## 配置详情
-
-### 默认代理配置
-
-**本地代理地址:** `http://127.0.0.1:9910`
-
-**配置文件位置:**
-- `~/.proxychains/proxychains.conf` (推荐)
-- `/etc/proxychains.conf` (系统级)
-
-### 快速配置脚本
-
-创建用户级配置(自动使用 127.0.0.1:9910):
-
-```bash
-mkdir -p ~/.proxychains
-cat > ~/.proxychains/proxychains.conf << 'EOF'
-# Proxychains configuration
-strict_chain
-proxy_dns
-remote_dns_subnet 224
-tcp_read_time_out 15000
-tcp_connect_time_out 8000
-
-[ProxyList]
-http 127.0.0.1 9910
-EOF
-```
-
-### 环境变量方式(临时使用)
-
-```bash
-# 设置代理环境变量
-export PROXYCHAINS_SOCKS5_HOST=127.0.0.1
-export PROXYCHAINS_SOCKS5_PORT=9910
-
-# 使用
-proxychains4 curl https://github.com
-```
-
-## 工作原理
-
-Proxychains 通过 LD_PRELOAD 机制拦截程序的网络调用:
-
-1. **拦截系统调用**:Hook socket 相关的 libc 函数
-2. **重定向连接**:将所有 TCP 连接重定向到代理服务器
-3. **DNS 代理**:通过代理服务器解析域名
-4. **透明代理**:应用程序无需修改
-
-**重要限制:**
-- 只支持动态链接的程序(statically linked 程序不支持)
-- 只支持 TCP 连接(UDP 不支持)
-- 部分使用特殊网络库的程序可能不兼容
-
-## Claude 自动化规则
-
-### 规则 1: 网络错误自动重试
-
-```
-IF 命令返回网络相关错误(timeout, connection refused, DNS failure)
-THEN 自动使用 proxychains4 重试该命令
-```
-
-### 规则 2: 已知慢速源强制代理
-
-```
-IF 访问以下域名/服务:
- - github.com
- - raw.githubusercontent.com
- - pypi.org
- - npmjs.org
- - registry.npmjs.org
- - docker.io
- - gcr.io
-THEN 直接使用 proxychains4(不等待失败)
-```
-
-### 规则 3: 失败提示
-
-```
-IF proxychains4 命令也失败
-THEN 提示用户:
- 1. 检查代理服务是否运行(127.0.0.1:9910)
- 2. 检查 proxychains 配置文件
- 3. 尝试其他代理地址
-```
-
-## 故障排除
-
-### 检查代理服务状态
-
-```bash
-# 测试代理是否可用
-curl -x http://127.0.0.1:9910 https://www.google.com
-
-# 检查端口是否监听
-netstat -tunlp | grep 9910
-# 或
-ss -tunlp | grep 9910
-```
-
-### 验证 proxychains 配置
-
-```bash
-# 测试配置是否正确
-proxychains4 curl https://ipinfo.io/json
-# 应该显示代理服务器的 IP,而不是本机 IP
-```
-
-### 常见错误处理
-
-**错误 1: "proxychains: command not found"**
-```bash
-# 安装 proxychains4
-sudo apt install proxychains4 # Debian/Ubuntu
-sudo yum install proxychains-ng # CentOS/RHEL
-```
-
-**错误 2: "timeout"**
-```bash
-# 检查代理地址配置是否正确
-cat ~/.proxychains/proxychains.conf | grep -A 2 "\[ProxyList\]"
-
-# 修改超时时间(在配置文件中)
-tcp_connect_time_out 15000
-tcp_read_time_out 30000
-```
-
-**错误 3: "can't read configuration file"**
-```bash
-# 创建配置文件
-mkdir -p ~/.proxychains
-cp /etc/proxychains.conf ~/.proxychains/proxychains.conf
-# 然后编辑配置
-```
-
-## 高级用法
-
-### 多代理链
-
-```conf
-# ~/.proxychains/proxychains.conf
-strict_chain # 按顺序使用所有代理
-
-[ProxyList]
-http 127.0.0.1 9910
-socks5 127.0.0.1 1080
-```
-
-### 动态代理链
-
-```conf
-dynamic_chain # 自动跳过死代理
-
-[ProxyList]
-http 127.0.0.1 9910
-http 127.0.0.1 8080
-socks5 127.0.0.1 1080
-```
-
-### 随机代理链
-
-```conf
-random_chain
-chain_len = 2 # 随机选择 2 个代理
-
-[ProxyList]
-http 127.0.0.1 9910
-socks5 127.0.0.1 1080
-socks5 127.0.0.1 1081
-```
-
-### 自定义 DNS 服务器
-
-```bash
-# 使用自定义 DNS 通过代理解析
-export PROXY_DNS_SERVER=8.8.8.8
-proxychains4 curl https://example.com
-```
-
-## 参考资源
-
-- **官方仓库**: https://github.com/haad/proxychains
-- **配置文件**: `references/proxychains.conf` (完整示例)
-- **故障排除**: `references/troubleshooting.md`
-- **命令速查**: `references/quick-reference.md`
-
-## 总结
-
-**记住这些原则:**
-1. ❌ **遇到网络错误** → ✅ 自动加上 `proxychains4`
-2. 🌐 **访问国外资源** → ✅ 主动使用 `proxychains4`
-3. 🔧 **代理也失败** → ✅ 提示用户检查代理服务
-
-**默认代理:** `http://127.0.0.1:9910`
-
----
-
-**这个技能让 Claude 在遇到网络问题时自动使用代理,无需用户手动干预!**
diff --git a/assets/skills/skills-skills/AGENTS.md b/assets/skills/skills-skills/AGENTS.md
deleted file mode 100644
index d005bab..0000000
--- a/assets/skills/skills-skills/AGENTS.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# assets/skills/skills-skills
-
-This directory is a **meta-skill**: it turns arbitrary domain material (docs/APIs/code/specs) into a reusable Skill (`SKILL.md` + `references/` + `scripts/` + `assets/`), and ships an executable quality gate + scaffolding.
-
-## Layout
-
-```
-skills-skills/
-|-- AGENTS.md
-|-- SKILL.md
-|-- assets/
-| |-- template-minimal.md
-| `-- template-complete.md
-|-- scripts/
-| |-- Skill_Seekers-development/
-| |-- create-skill.sh
-| |-- skill-seekers-bootstrap.sh
-| |-- skill-seekers-configs -> Skill_Seekers-development/configs
-| |-- skill-seekers-import.sh
-| |-- skill-seekers.sh
-| |-- skill-seekers-src -> Skill_Seekers-development/src
-| |-- skill-seekers-update.sh
-| `-- validate-skill.sh
-`-- references/
- |-- index.md
- |-- README.md
- |-- anti-patterns.md
- |-- skill-seekers.md
- |-- quality-checklist.md
- `-- skill-spec.md
-```
-
-## File Responsibilities
-
-- `assets/skills/skills-skills/SKILL.md`: entrypoint (triggers, deliverables, workflow, quality gate, tooling).
-- `assets/skills/skills-skills/assets/template-minimal.md`: minimal template (small domains / quick bootstrap).
-- `assets/skills/skills-skills/assets/template-complete.md`: full template (production-grade / complex domains).
-- `assets/skills/skills-skills/scripts/create-skill.sh`: scaffold generator (minimal/full, output dir, overwrite).
-- `assets/skills/skills-skills/scripts/Skill_Seekers-development/`: vendored Skill Seekers source snapshot (code + configs; excludes upstream Markdown docs).
-- `assets/skills/skills-skills/scripts/skill-seekers-bootstrap.sh`: create a local venv and install deps for the vendored Skill Seekers tool.
-- `assets/skills/skills-skills/scripts/skill-seekers.sh`: run Skill Seekers from vendored source (docs/github/pdf -> output//).
-- `assets/skills/skills-skills/scripts/skill-seekers-import.sh`: import output// into the canonical assets/skills// tree.
-- `assets/skills/skills-skills/scripts/skill-seekers-update.sh`: update the vendored source snapshot from upstream (network required).
-- `assets/skills/skills-skills/scripts/validate-skill.sh`: spec validator (supports `--strict`).
-- `assets/skills/skills-skills/references/index.md`: navigation for this meta-skill's reference docs.
-- `assets/skills/skills-skills/references/README.md`: upstream official reference (lightly adjusted to keep links working in this repo).
-- `assets/skills/skills-skills/references/skill-spec.md`: the local Skill spec (MUST/SHOULD/NEVER).
-- `assets/skills/skills-skills/references/quality-checklist.md`: quality gate checklist + scoring.
-- `assets/skills/skills-skills/references/anti-patterns.md`: common failure modes and how to fix them.
-- `assets/skills/skills-skills/references/skill-seekers.md`: how to use the vendored tool as a mandatory first-draft generator.
-
-## Dependencies & Boundaries
-
-- `scripts/*.sh`: depend on `bash` + common POSIX tooling; some scripts require extra tooling:
- - `skill-seekers-bootstrap.sh`: requires `python3` + `pip` (network required for PyPI).
- - `skill-seekers-update.sh`: requires `curl` + `tar` + `rsync` (network required).
-- This directory is about "how to build Skills", not about any specific domain; domain knowledge belongs in `assets/skills//`.
diff --git a/assets/skills/skills-skills/references/skill-seekers.md b/assets/skills/skills-skills/references/skill-seekers.md
deleted file mode 100644
index 87451ad..0000000
--- a/assets/skills/skills-skills/references/skill-seekers.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Skill Seekers(内置工具)使用说明
-
-本目录把 `Skill_Seekers-development` 的源码作为 `skills-skills` 的必备工具内置,用于把「文档 / GitHub 仓库 / PDF」快速转成一个可落地的 Skill 初稿。
-
-## 目录约定
-
-- 工具源码:`assets/skills/skills-skills/scripts/Skill_Seekers-development/`
-- 运行入口:`assets/skills/skills-skills/scripts/skill-seekers.sh`
-- 依赖初始化:`assets/skills/skills-skills/scripts/skill-seekers-bootstrap.sh`
-- 导入到本仓库:`assets/skills/skills-skills/scripts/skill-seekers-import.sh`
-- 更新源码快照:`assets/skills/skills-skills/scripts/skill-seekers-update.sh`(需要网络)
-
-## 推荐工作流(强约束)
-
-1. 用 Skill Seekers 生成初稿到 `output//`
-2. 导入到 `assets/skills//`
-3. 用 `validate-skill.sh --strict` 做质量闸门
-4. 回到 `skills-skills` 的规范对 `SKILL.md` 做“可激活性”与“边界”修订
-
-## 最小可执行示例
-
-```bash
-# 1) 初始化(只需一次)
-./assets/skills/skills-skills/scripts/skill-seekers-bootstrap.sh
-
-# 2) 生成(示例:抓 docs 配置)
-./assets/skills/skills-skills/scripts/skill-seekers.sh -- scrape --config ./assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react.json
-
-# 3) 导入到 skills/
-./assets/skills/skills-skills/scripts/skill-seekers-import.sh react
-
-# 4) 严格校验
-./assets/skills/skills-skills/scripts/validate-skill.sh assets/skills/react --strict
-```
-
-## 设计原则
-
-- `skills/skills-skills/` 负责:规范、模板、闸门、可激活性;不直接承载领域知识。
-- Skill Seekers 负责:抓取与初稿生成;最终交付仍以本仓库的 `validate-skill.sh --strict` 为准。
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/LICENSE b/assets/skills/skills-skills/scripts/Skill_Seekers-development/LICENSE
deleted file mode 100644
index 11d6561..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2025 [Your Name/Username]
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/ansible-core.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/ansible-core.json
deleted file mode 100644
index 764cead..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/ansible-core.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "name": "ansible-core",
- "description": "Ansible Core 2.19 skill for automation and configuration management",
- "base_url": "https://docs.ansible.com/ansible-core/2.19/",
- "selectors": {
- "main_content": "div[role=main]",
- "title": "title",
- "code_blocks": "pre"
- },
- "url_patterns": {
- "include": [],
- "exclude": ["/_static/", "/_images/", "/_downloads/", "/search.html", "/genindex.html", "/py-modindex.html", "/index.html", "/roadmap/"]
- },
- "categories": {
- "getting_started": ["getting_started", "getting-started", "introduction", "overview"],
- "installation": ["installation_guide", "installation", "setup"],
- "inventory": ["inventory_guide", "inventory"],
- "playbooks": ["playbook_guide", "playbooks", "playbook"],
- "modules": ["module_plugin_guide", "modules", "plugins"],
- "collections": ["collections_guide", "collections"],
- "vault": ["vault_guide", "vault", "encryption"],
- "commands": ["command_guide", "commands", "cli"],
- "porting": ["porting_guides", "porting", "migration"],
- "os_specific": ["os_guide", "platform"],
- "tips": ["tips_tricks", "tips", "tricks", "best-practices"],
- "community": ["community", "contributing", "contributions"],
- "development": ["dev_guide", "development", "developing"]
- },
- "rate_limit": 0.5,
- "max_pages": 800
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/astro.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/astro.json
deleted file mode 100644
index 89b2798..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/astro.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "name": "astro",
- "description": "Astro web framework for content-focused websites. Use for Astro components, islands architecture, content collections, SSR/SSG, and modern web development.",
- "base_url": "https://docs.astro.build/en/getting-started/",
- "start_urls": [
- "https://docs.astro.build/en/getting-started/",
- "https://docs.astro.build/en/install/auto/",
- "https://docs.astro.build/en/core-concepts/project-structure/",
- "https://docs.astro.build/en/core-concepts/astro-components/",
- "https://docs.astro.build/en/core-concepts/astro-pages/"
- ],
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": ["/en/"],
- "exclude": ["/blog", "/integrations"]
- },
- "categories": {
- "getting_started": ["getting-started", "install", "tutorial"],
- "core_concepts": ["core-concepts", "project-structure", "components", "pages"],
- "guides": ["guides", "deploy", "migrate"],
- "configuration": ["configuration", "config", "typescript"],
- "integrations": ["integrations", "framework", "adapter"]
- },
- "rate_limit": 0.5,
- "max_pages": 100
-}
\ No newline at end of file
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/claude-code.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/claude-code.json
deleted file mode 100644
index c84e709..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/claude-code.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "name": "claude-code",
- "description": "Claude Code CLI and development environment. Use for Claude Code features, tools, workflows, MCP integration, configuration, and AI-assisted development.",
- "base_url": "https://docs.claude.com/en/docs/claude-code/",
- "start_urls": [
- "https://docs.claude.com/en/docs/claude-code/overview",
- "https://docs.claude.com/en/docs/claude-code/quickstart",
- "https://docs.claude.com/en/docs/claude-code/common-workflows",
- "https://docs.claude.com/en/docs/claude-code/mcp",
- "https://docs.claude.com/en/docs/claude-code/settings",
- "https://docs.claude.com/en/docs/claude-code/troubleshooting",
- "https://docs.claude.com/en/docs/claude-code/iam"
- ],
- "selectors": {
- "main_content": "#content-container",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": ["/claude-code/"],
- "exclude": ["/api-reference/", "/claude-ai/", "/claude.ai/", "/prompt-engineering/", "/changelog/"]
- },
- "categories": {
- "getting_started": ["overview", "quickstart", "installation", "setup", "terminal-config"],
- "workflows": ["workflow", "common-workflows", "git", "testing", "debugging", "interactive"],
- "mcp": ["mcp", "model-context-protocol"],
- "configuration": ["config", "settings", "preferences", "customize", "hooks", "statusline", "model-config", "memory", "output-styles"],
- "agents": ["agent", "task", "subagent", "sub-agent", "specialized"],
- "skills": ["skill", "agent-skill"],
- "integrations": ["ide-integrations", "vs-code", "jetbrains", "plugin", "marketplace"],
- "deployment": ["bedrock", "vertex", "deployment", "network", "gateway", "devcontainer", "sandboxing", "third-party"],
- "reference": ["reference", "api", "command", "cli-reference", "slash", "checkpointing", "headless", "sdk"],
- "enterprise": ["iam", "security", "monitoring", "analytics", "costs", "legal", "data-usage"]
- },
- "rate_limit": 0.5,
- "max_pages": 200
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/django.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/django.json
deleted file mode 100644
index 70f84b6..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/django.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "django",
- "description": "Django web framework for Python. Use for Django models, views, templates, ORM, authentication, and web development.",
- "base_url": "https://docs.djangoproject.com/en/stable/",
- "start_urls": [
- "https://docs.djangoproject.com/en/stable/intro/",
- "https://docs.djangoproject.com/en/stable/topics/db/models/",
- "https://docs.djangoproject.com/en/stable/topics/http/views/",
- "https://docs.djangoproject.com/en/stable/topics/templates/",
- "https://docs.djangoproject.com/en/stable/topics/forms/",
- "https://docs.djangoproject.com/en/stable/topics/auth/",
- "https://docs.djangoproject.com/en/stable/ref/models/"
- ],
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre"
- },
- "url_patterns": {
- "include": ["/intro/", "/topics/", "/ref/", "/howto/"],
- "exclude": ["/faq/", "/misc/", "/releases/"]
- },
- "categories": {
- "getting_started": ["intro", "tutorial", "install"],
- "models": ["models", "database", "orm", "queries"],
- "views": ["views", "urlconf", "routing"],
- "templates": ["templates", "template"],
- "forms": ["forms", "form"],
- "authentication": ["auth", "authentication", "user"],
- "api": ["ref", "reference"]
- },
- "rate_limit": 0.3,
- "max_pages": 500
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/django_unified.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/django_unified.json
deleted file mode 100644
index 7bb2db2..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/django_unified.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "name": "django",
- "description": "Complete Django framework knowledge combining official documentation and Django codebase. Use when building Django applications, understanding ORM internals, or debugging Django issues.",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://docs.djangoproject.com/en/stable/",
- "extract_api": true,
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre"
- },
- "url_patterns": {
- "include": [],
- "exclude": ["/search/", "/genindex/"]
- },
- "categories": {
- "getting_started": ["intro", "tutorial", "install"],
- "models": ["models", "orm", "queries", "database"],
- "views": ["views", "urls", "templates"],
- "forms": ["forms", "modelforms"],
- "admin": ["admin"],
- "api": ["ref/"],
- "topics": ["topics/"],
- "security": ["security", "csrf", "authentication"]
- },
- "rate_limit": 0.5,
- "max_pages": 300
- },
- {
- "type": "github",
- "repo": "django/django",
- "include_issues": true,
- "max_issues": 100,
- "include_changelog": true,
- "include_releases": true,
- "include_code": true,
- "code_analysis_depth": "surface",
- "file_patterns": [
- "django/db/**/*.py",
- "django/views/**/*.py",
- "django/forms/**/*.py",
- "django/contrib/admin/**/*.py"
- ]
- }
- ]
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/example_pdf.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/example_pdf.json
deleted file mode 100644
index 08c7475..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/example_pdf.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "name": "example_manual",
- "description": "Example PDF documentation skill",
- "pdf_path": "docs/manual.pdf",
- "extract_options": {
- "chunk_size": 10,
- "min_quality": 5.0,
- "extract_images": true,
- "min_image_size": 100
- },
- "categories": {
- "getting_started": ["introduction", "getting started", "quick start", "setup"],
- "tutorial": ["tutorial", "guide", "walkthrough", "example"],
- "api": ["api", "reference", "function", "class", "method"],
- "advanced": ["advanced", "optimization", "performance", "best practices"]
- }
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/fastapi.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/fastapi.json
deleted file mode 100644
index f08a08c..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/fastapi.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "name": "fastapi",
- "description": "FastAPI modern Python web framework. Use for building APIs, async endpoints, dependency injection, and Python backend development.",
- "base_url": "https://fastapi.tiangolo.com/",
- "start_urls": [
- "https://fastapi.tiangolo.com/tutorial/",
- "https://fastapi.tiangolo.com/tutorial/first-steps/",
- "https://fastapi.tiangolo.com/tutorial/path-params/",
- "https://fastapi.tiangolo.com/tutorial/body/",
- "https://fastapi.tiangolo.com/tutorial/dependencies/",
- "https://fastapi.tiangolo.com/advanced/",
- "https://fastapi.tiangolo.com/reference/"
- ],
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": ["/tutorial/", "/advanced/", "/reference/"],
- "exclude": ["/help/", "/external-links/", "/deployment/"]
- },
- "categories": {
- "getting_started": ["first-steps", "tutorial", "intro"],
- "path_operations": ["path", "operations", "routing"],
- "request_data": ["request", "body", "query", "parameters"],
- "dependencies": ["dependencies", "injection"],
- "security": ["security", "oauth", "authentication"],
- "database": ["database", "sql", "orm"]
- },
- "rate_limit": 0.5,
- "max_pages": 250
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/fastapi_unified.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/fastapi_unified.json
deleted file mode 100644
index 6f76b9e..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/fastapi_unified.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "name": "fastapi",
- "description": "Complete FastAPI knowledge combining official documentation and FastAPI codebase. Use when building FastAPI applications, understanding async patterns, or working with Pydantic models.",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://fastapi.tiangolo.com/",
- "extract_api": true,
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [],
- "exclude": ["/img/", "/js/"]
- },
- "categories": {
- "getting_started": ["tutorial", "first-steps"],
- "path_operations": ["path-params", "query-params", "body"],
- "dependencies": ["dependencies"],
- "security": ["security", "oauth2"],
- "database": ["sql-databases"],
- "advanced": ["advanced", "async", "middleware"],
- "deployment": ["deployment"]
- },
- "rate_limit": 0.5,
- "max_pages": 150
- },
- {
- "type": "github",
- "repo": "tiangolo/fastapi",
- "include_issues": true,
- "max_issues": 100,
- "include_changelog": true,
- "include_releases": true,
- "include_code": true,
- "code_analysis_depth": "surface",
- "file_patterns": [
- "fastapi/**/*.py"
- ]
- }
- ]
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/fastapi_unified_test.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/fastapi_unified_test.json
deleted file mode 100644
index cd18825..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/fastapi_unified_test.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "name": "fastapi_test",
- "description": "FastAPI test - unified scraping with limited pages",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://fastapi.tiangolo.com/",
- "extract_api": true,
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [],
- "exclude": ["/img/", "/js/"]
- },
- "categories": {
- "getting_started": ["tutorial", "first-steps"],
- "path_operations": ["path-params", "query-params"],
- "api": ["reference"]
- },
- "rate_limit": 0.5,
- "max_pages": 20
- },
- {
- "type": "github",
- "repo": "tiangolo/fastapi",
- "include_issues": false,
- "include_changelog": false,
- "include_releases": true,
- "include_code": true,
- "code_analysis_depth": "surface",
- "file_patterns": [
- "fastapi/routing.py",
- "fastapi/applications.py"
- ]
- }
- ]
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot-large-example.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot-large-example.json
deleted file mode 100644
index a4d04b9..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot-large-example.json
+++ /dev/null
@@ -1,63 +0,0 @@
-{
- "name": "godot",
- "description": "Godot Engine game development. Use for Godot projects, GDScript/C# coding, scene setup, node systems, 2D/3D development, physics, animation, UI, shaders, or any Godot-specific questions.",
- "base_url": "https://docs.godotengine.org/en/stable/",
- "start_urls": [
- "https://docs.godotengine.org/en/stable/getting_started/introduction/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/2d/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/3d/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/physics/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/animation/index.html",
- "https://docs.godotengine.org/en/stable/classes/index.html"
- ],
- "selectors": {
- "main_content": "div[role='main']",
- "title": "title",
- "code_blocks": "pre"
- },
- "url_patterns": {
- "include": [
- "/getting_started/",
- "/tutorials/",
- "/classes/"
- ],
- "exclude": [
- "/genindex.html",
- "/search.html",
- "/_static/",
- "/_sources/"
- ]
- },
- "categories": {
- "getting_started": ["introduction", "getting_started", "first", "your_first"],
- "scripting": ["scripting", "gdscript", "c#", "csharp"],
- "2d": ["/2d/", "sprite", "canvas", "tilemap"],
- "3d": ["/3d/", "spatial", "mesh", "3d_"],
- "physics": ["physics", "collision", "rigidbody", "characterbody"],
- "animation": ["animation", "tween", "animationplayer"],
- "ui": ["ui", "control", "gui", "theme"],
- "shaders": ["shader", "material", "visual_shader"],
- "audio": ["audio", "sound"],
- "networking": ["networking", "multiplayer", "rpc"],
- "export": ["export", "platform", "deploy"]
- },
- "rate_limit": 0.5,
- "max_pages": 40000,
-
- "_comment": "=== NEW: Split Strategy Configuration ===",
- "split_strategy": "router",
- "split_config": {
- "target_pages_per_skill": 5000,
- "create_router": true,
- "split_by_categories": ["scripting", "2d", "3d", "physics", "shaders"],
- "router_name": "godot",
- "parallel_scraping": true
- },
-
- "_comment2": "=== NEW: Checkpoint Configuration ===",
- "checkpoint": {
- "enabled": true,
- "interval": 1000
- }
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot.json
deleted file mode 100644
index acd49f2..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "name": "godot",
- "description": "Godot Engine game development. Use for Godot projects, GDScript/C# coding, scene setup, node systems, 2D/3D development, physics, animation, UI, shaders, or any Godot-specific questions.",
- "base_url": "https://docs.godotengine.org/en/stable/",
- "start_urls": [
- "https://docs.godotengine.org/en/stable/getting_started/introduction/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/2d/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/3d/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/physics/index.html",
- "https://docs.godotengine.org/en/stable/tutorials/animation/index.html",
- "https://docs.godotengine.org/en/stable/classes/index.html"
- ],
- "selectors": {
- "main_content": "div[role='main']",
- "title": "title",
- "code_blocks": "pre"
- },
- "url_patterns": {
- "include": [
- "/getting_started/",
- "/tutorials/",
- "/classes/"
- ],
- "exclude": [
- "/genindex.html",
- "/search.html",
- "/_static/",
- "/_sources/"
- ]
- },
- "categories": {
- "getting_started": ["introduction", "getting_started", "first", "your_first"],
- "scripting": ["scripting", "gdscript", "c#", "csharp"],
- "2d": ["/2d/", "sprite", "canvas", "tilemap"],
- "3d": ["/3d/", "spatial", "mesh", "3d_"],
- "physics": ["physics", "collision", "rigidbody", "characterbody"],
- "animation": ["animation", "tween", "animationplayer"],
- "ui": ["ui", "control", "gui", "theme"],
- "shaders": ["shader", "material", "visual_shader"],
- "audio": ["audio", "sound"],
- "networking": ["networking", "multiplayer", "rpc"],
- "export": ["export", "platform", "deploy"]
- },
- "rate_limit": 0.5,
- "max_pages": 500
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot_github.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot_github.json
deleted file mode 100644
index e33c66f..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot_github.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "godot",
- "repo": "godotengine/godot",
- "description": "Godot Engine - Multi-platform 2D and 3D game engine",
- "github_token": null,
- "include_issues": true,
- "max_issues": 100,
- "include_changelog": true,
- "include_releases": true,
- "include_code": false,
- "file_patterns": [
- "core/**/*.h",
- "core/**/*.cpp",
- "scene/**/*.h",
- "scene/**/*.cpp",
- "servers/**/*.h",
- "servers/**/*.cpp"
- ]
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot_unified.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot_unified.json
deleted file mode 100644
index 3366dea..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/godot_unified.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "name": "godot",
- "description": "Complete Godot Engine knowledge base combining official documentation and source code analysis",
- "merge_mode": "claude-enhanced",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://docs.godotengine.org/en/stable/",
- "extract_api": true,
- "selectors": {
- "main_content": "div[role='main']",
- "title": "title",
- "code_blocks": "pre"
- },
- "url_patterns": {
- "include": [],
- "exclude": ["/search.html", "/_static/", "/_images/"]
- },
- "categories": {
- "getting_started": ["introduction", "getting_started", "step_by_step"],
- "scripting": ["scripting", "gdscript", "c_sharp"],
- "2d": ["2d", "canvas", "sprite", "animation"],
- "3d": ["3d", "spatial", "mesh", "shader"],
- "physics": ["physics", "collision", "rigidbody"],
- "api": ["api", "class", "reference", "method"]
- },
- "rate_limit": 0.5,
- "max_pages": 500
- },
- {
- "type": "github",
- "repo": "godotengine/godot",
- "github_token": null,
- "code_analysis_depth": "deep",
- "include_code": true,
- "include_issues": true,
- "max_issues": 100,
- "include_changelog": true,
- "include_releases": true,
- "file_patterns": [
- "core/**/*.h",
- "core/**/*.cpp",
- "scene/**/*.h",
- "scene/**/*.cpp",
- "servers/**/*.h",
- "servers/**/*.cpp"
- ]
- }
- ]
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/hono.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/hono.json
deleted file mode 100644
index e27ca41..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/hono.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "hono",
- "description": "Hono web application framework for building fast, lightweight APIs. Use for Hono routing, middleware, context handling, and modern JavaScript/TypeScript web development.",
- "llms_txt_url": "https://hono.dev/llms-full.txt",
- "base_url": "https://hono.dev/docs",
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [],
- "exclude": []
- },
- "categories": {},
- "rate_limit": 0.5,
- "max_pages": 50
-}
\ No newline at end of file
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/kubernetes.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/kubernetes.json
deleted file mode 100644
index 717794b..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/kubernetes.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
- "name": "kubernetes",
- "description": "Kubernetes container orchestration platform. Use for K8s clusters, deployments, pods, services, networking, storage, configuration, and DevOps tasks.",
- "base_url": "https://kubernetes.io/docs/",
- "start_urls": [
- "https://kubernetes.io/docs/home/",
- "https://kubernetes.io/docs/concepts/",
- "https://kubernetes.io/docs/tasks/",
- "https://kubernetes.io/docs/tutorials/",
- "https://kubernetes.io/docs/reference/"
- ],
- "selectors": {
- "main_content": "main",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [
- "/docs/concepts/",
- "/docs/tasks/",
- "/docs/tutorials/",
- "/docs/reference/",
- "/docs/setup/"
- ],
- "exclude": [
- "/search/",
- "/blog/",
- "/training/",
- "/partners/",
- "/community/",
- "/_print/",
- "/case-studies/"
- ]
- },
- "categories": {
- "getting_started": ["getting-started", "setup", "learning-environment"],
- "concepts": ["concepts", "overview", "architecture"],
- "workloads": ["workloads", "pods", "deployments", "replicaset", "statefulset", "daemonset"],
- "services": ["services", "networking", "ingress", "service"],
- "storage": ["storage", "volumes", "persistent"],
- "configuration": ["configuration", "configmap", "secret"],
- "security": ["security", "rbac", "policies", "authentication"],
- "tasks": ["tasks", "administer", "configure"],
- "tutorials": ["tutorials", "stateless", "stateful"]
- },
- "rate_limit": 0.5,
- "max_pages": 1000
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/laravel.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/laravel.json
deleted file mode 100644
index f68c9bf..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/laravel.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "laravel",
- "description": "Laravel PHP web framework. Use for Laravel models, routes, controllers, Blade templates, Eloquent ORM, authentication, and PHP web development.",
- "base_url": "https://laravel.com/docs/9.x/",
- "start_urls": [
- "https://laravel.com/docs/9.x/installation",
- "https://laravel.com/docs/9.x/routing",
- "https://laravel.com/docs/9.x/controllers",
- "https://laravel.com/docs/9.x/views",
- "https://laravel.com/docs/9.x/blade",
- "https://laravel.com/docs/9.x/eloquent",
- "https://laravel.com/docs/9.x/migrations",
- "https://laravel.com/docs/9.x/authentication"
- ],
- "selectors": {
- "main_content": "#main-content",
- "title": "h1",
- "code_blocks": "pre"
- },
- "url_patterns": {
- "include": ["/docs/9.x/", "/docs/10.x/", "/docs/11.x/"],
- "exclude": ["/api/", "/packages/"]
- },
- "categories": {
- "getting_started": ["installation", "configuration", "structure", "deployment"],
- "routing": ["routing", "middleware", "controllers"],
- "views": ["views", "blade", "templates"],
- "models": ["eloquent", "database", "migrations", "seeding", "queries"],
- "authentication": ["authentication", "authorization", "passwords"],
- "api": ["api", "resources", "requests", "responses"]
- },
- "rate_limit": 0.3,
- "max_pages": 500
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/python-tutorial-test.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/python-tutorial-test.json
deleted file mode 100644
index 240b0be..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/python-tutorial-test.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "name": "python-tutorial-test",
- "description": "Python tutorial for testing MCP tools",
- "base_url": "https://docs.python.org/3/tutorial/",
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [],
- "exclude": []
- },
- "categories": {},
- "rate_limit": 0.3,
- "max_pages": 10
-}
\ No newline at end of file
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react.json
deleted file mode 100644
index e6f4c92..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "name": "react",
- "description": "React framework for building user interfaces. Use for React components, hooks, state management, JSX, and modern frontend development.",
- "base_url": "https://react.dev/",
- "start_urls": [
- "https://react.dev/learn",
- "https://react.dev/learn/quick-start",
- "https://react.dev/learn/thinking-in-react",
- "https://react.dev/reference/react",
- "https://react.dev/reference/react-dom",
- "https://react.dev/reference/react/hooks"
- ],
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": ["/learn", "/reference"],
- "exclude": ["/community", "/blog"]
- },
- "categories": {
- "getting_started": ["quick-start", "installation", "tutorial"],
- "hooks": ["usestate", "useeffect", "usememo", "usecallback", "usecontext", "useref", "hook"],
- "components": ["component", "props", "jsx"],
- "state": ["state", "context", "reducer"],
- "api": ["api", "reference"]
- },
- "rate_limit": 0.5,
- "max_pages": 300
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react_github.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react_github.json
deleted file mode 100644
index 4c8b86a..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react_github.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "react",
- "repo": "facebook/react",
- "description": "React JavaScript library for building user interfaces",
- "github_token": null,
- "include_issues": true,
- "max_issues": 100,
- "include_changelog": true,
- "include_releases": true,
- "include_code": false,
- "file_patterns": [
- "packages/**/*.js",
- "packages/**/*.ts"
- ]
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react_unified.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react_unified.json
deleted file mode 100644
index 437bd1d..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/react_unified.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "name": "react",
- "description": "Complete React knowledge base combining official documentation and React codebase insights. Use when working with React, understanding API changes, or debugging React internals.",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://react.dev/",
- "extract_api": true,
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [],
- "exclude": ["/blog/", "/community/"]
- },
- "categories": {
- "getting_started": ["learn", "installation", "quick-start"],
- "components": ["components", "props", "state"],
- "hooks": ["hooks", "usestate", "useeffect", "usecontext"],
- "api": ["api", "reference"],
- "advanced": ["context", "refs", "portals", "suspense"]
- },
- "rate_limit": 0.5,
- "max_pages": 200
- },
- {
- "type": "github",
- "repo": "facebook/react",
- "include_issues": true,
- "max_issues": 100,
- "include_changelog": true,
- "include_releases": true,
- "include_code": true,
- "code_analysis_depth": "surface",
- "file_patterns": [
- "packages/react/src/**/*.js",
- "packages/react-dom/src/**/*.js"
- ]
- }
- ]
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/steam-economy-complete.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/steam-economy-complete.json
deleted file mode 100644
index 2642cd9..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/steam-economy-complete.json
+++ /dev/null
@@ -1,108 +0,0 @@
-{
- "name": "steam-economy-complete",
- "description": "Complete Steam Economy system including inventory, microtransactions, trading, and monetization. Use for ISteamInventory API, ISteamEconomy API, IInventoryService Web API, Steam Wallet integration, in-app purchases, item definitions, trading, crafting, market integration, and all economy features for game developers.",
- "base_url": "https://partner.steamgames.com/doc/",
- "start_urls": [
- "https://partner.steamgames.com/doc/features/inventory",
- "https://partner.steamgames.com/doc/features/microtransactions",
- "https://partner.steamgames.com/doc/features/microtransactions/implementation",
- "https://partner.steamgames.com/doc/api/ISteamInventory",
- "https://partner.steamgames.com/doc/webapi/ISteamEconomy",
- "https://partner.steamgames.com/doc/webapi/IInventoryService",
- "https://partner.steamgames.com/doc/features/inventory/economy"
- ],
- "selectors": {
- "main_content": "div.documentation_bbcode",
- "title": "div.docPageTitle",
- "code_blocks": "div.bb_code"
- },
- "url_patterns": {
- "include": [
- "/features/inventory",
- "/features/microtransactions",
- "/api/ISteamInventory",
- "/webapi/ISteamEconomy",
- "/webapi/IInventoryService"
- ],
- "exclude": [
- "/home",
- "/sales",
- "/marketing",
- "/legal",
- "/finance",
- "/login",
- "/search",
- "/steamworks/apps",
- "/steamworks/partner"
- ]
- },
- "categories": {
- "getting_started": [
- "overview",
- "getting started",
- "introduction",
- "quickstart",
- "setup"
- ],
- "inventory_system": [
- "inventory",
- "item definition",
- "item schema",
- "item properties",
- "itemdefs",
- "ISteamInventory"
- ],
- "microtransactions": [
- "microtransaction",
- "purchase",
- "payment",
- "checkout",
- "wallet",
- "transaction"
- ],
- "economy_api": [
- "ISteamEconomy",
- "economy",
- "asset",
- "context"
- ],
- "inventory_webapi": [
- "IInventoryService",
- "webapi",
- "web api",
- "http"
- ],
- "trading": [
- "trading",
- "trade",
- "exchange",
- "market"
- ],
- "crafting": [
- "crafting",
- "recipe",
- "combine",
- "exchange"
- ],
- "pricing": [
- "pricing",
- "price",
- "cost",
- "currency"
- ],
- "implementation": [
- "integration",
- "implementation",
- "configure",
- "best practices"
- ],
- "examples": [
- "example",
- "sample",
- "tutorial",
- "walkthrough"
- ]
- },
- "rate_limit": 0.7,
- "max_pages": 1000
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/tailwind.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/tailwind.json
deleted file mode 100644
index 38a11d7..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/tailwind.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "name": "tailwind",
- "description": "Tailwind CSS utility-first framework for rapid UI development. Use for Tailwind utilities, responsive design, custom configurations, and modern CSS workflows.",
- "base_url": "https://tailwindcss.com/docs",
- "start_urls": [
- "https://tailwindcss.com/docs/installation",
- "https://tailwindcss.com/docs/utility-first",
- "https://tailwindcss.com/docs/responsive-design",
- "https://tailwindcss.com/docs/hover-focus-and-other-states"
- ],
- "selectors": {
- "main_content": "div.prose",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": ["/docs"],
- "exclude": ["/blog", "/resources"]
- },
- "categories": {
- "getting_started": ["installation", "editor-setup", "intellisense"],
- "core_concepts": ["utility-first", "responsive", "hover-focus", "dark-mode"],
- "layout": ["container", "columns", "flex", "grid"],
- "typography": ["font-family", "font-size", "text-align", "text-color"],
- "backgrounds": ["background-color", "background-image", "gradient"],
- "customization": ["configuration", "theme", "plugins"]
- },
- "rate_limit": 0.5,
- "max_pages": 100
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/test-manual.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/test-manual.json
deleted file mode 100644
index cfbcba5..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/test-manual.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "name": "test-manual",
- "description": "Manual test config",
- "base_url": "https://test.example.com/",
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [],
- "exclude": []
- },
- "categories": {},
- "rate_limit": 0.5,
- "max_pages": 50
-}
\ No newline at end of file
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/vue.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/vue.json
deleted file mode 100644
index dc39d13..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/configs/vue.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "name": "vue",
- "description": "Vue.js progressive JavaScript framework. Use for Vue components, reactivity, composition API, and frontend development.",
- "base_url": "https://vuejs.org/",
- "start_urls": [
- "https://vuejs.org/guide/introduction.html",
- "https://vuejs.org/guide/quick-start.html",
- "https://vuejs.org/guide/essentials/application.html",
- "https://vuejs.org/guide/components/registration.html",
- "https://vuejs.org/guide/reusability/composables.html",
- "https://vuejs.org/api/"
- ],
- "selectors": {
- "main_content": "main",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": ["/guide/", "/api/", "/examples/"],
- "exclude": ["/about/", "/sponsor/", "/partners/"]
- },
- "categories": {
- "getting_started": ["quick-start", "introduction", "essentials"],
- "components": ["component", "props", "events"],
- "reactivity": ["reactivity", "reactive", "ref", "computed"],
- "composition_api": ["composition", "setup"],
- "api": ["api", "reference"]
- },
- "rate_limit": 0.5,
- "max_pages": 200
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/demo_conflicts.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/demo_conflicts.py
deleted file mode 100644
index 776ad50..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/demo_conflicts.py
+++ /dev/null
@@ -1,195 +0,0 @@
-#!/usr/bin/env python3
-"""
-Demo: Conflict Detection and Reporting
-
-This demonstrates the unified scraper's ability to detect and report
-conflicts between documentation and code implementation.
-"""
-
-import sys
-import json
-from pathlib import Path
-
-# Add CLI to path
-sys.path.insert(0, str(Path(__file__).parent / 'cli'))
-
-from conflict_detector import ConflictDetector
-
-print("=" * 70)
-print("UNIFIED SCRAPER - CONFLICT DETECTION DEMO")
-print("=" * 70)
-print()
-
-# Load test data
-print("📂 Loading test data...")
-print(" - Documentation APIs from example docs")
-print(" - Code APIs from example repository")
-print()
-
-with open('cli/conflicts.json', 'r') as f:
- conflicts_data = json.load(f)
-
-conflicts = conflicts_data['conflicts']
-summary = conflicts_data['summary']
-
-print(f"✅ Loaded {summary['total']} conflicts")
-print()
-
-# Display summary
-print("=" * 70)
-print("CONFLICT SUMMARY")
-print("=" * 70)
-print()
-
-print(f"📊 **Total Conflicts**: {summary['total']}")
-print()
-
-print("**By Type:**")
-for conflict_type, count in summary['by_type'].items():
- if count > 0:
- emoji = "📖" if conflict_type == "missing_in_docs" else "💻" if conflict_type == "missing_in_code" else "⚠️"
- print(f" {emoji} {conflict_type}: {count}")
-print()
-
-print("**By Severity:**")
-for severity, count in summary['by_severity'].items():
- if count > 0:
- emoji = "🔴" if severity == "high" else "🟡" if severity == "medium" else "🟢"
- print(f" {emoji} {severity.upper()}: {count}")
-print()
-
-# Display detailed conflicts
-print("=" * 70)
-print("DETAILED CONFLICT REPORTS")
-print("=" * 70)
-print()
-
-# Group by severity
-high = [c for c in conflicts if c['severity'] == 'high']
-medium = [c for c in conflicts if c['severity'] == 'medium']
-low = [c for c in conflicts if c['severity'] == 'low']
-
-# Show high severity first
-if high:
- print("🔴 **HIGH SEVERITY CONFLICTS** (Requires immediate attention)")
- print("-" * 70)
- for conflict in high:
- print()
- print(f"**API**: `{conflict['api_name']}`")
- print(f"**Type**: {conflict['type']}")
- print(f"**Issue**: {conflict['difference']}")
- print(f"**Suggestion**: {conflict['suggestion']}")
-
- if conflict['docs_info']:
- print(f"\n**Documented as**:")
- print(f" Signature: {conflict['docs_info'].get('raw_signature', 'N/A')}")
-
- if conflict['code_info']:
- print(f"\n**Implemented as**:")
- params = conflict['code_info'].get('parameters', [])
- param_str = ', '.join(f"{p['name']}: {p.get('type_hint', 'Any')}" for p in params if p['name'] != 'self')
- print(f" Signature: {conflict['code_info']['name']}({param_str})")
- print(f" Return type: {conflict['code_info'].get('return_type', 'None')}")
- print(f" Location: {conflict['code_info'].get('source', 'N/A')}:{conflict['code_info'].get('line', '?')}")
- print()
-
-# Show medium severity
-if medium:
- print("🟡 **MEDIUM SEVERITY CONFLICTS** (Review recommended)")
- print("-" * 70)
- for conflict in medium[:3]: # Show first 3
- print()
- print(f"**API**: `{conflict['api_name']}`")
- print(f"**Type**: {conflict['type']}")
- print(f"**Issue**: {conflict['difference']}")
-
- if conflict['code_info']:
- print(f"**Location**: {conflict['code_info'].get('source', 'N/A')}")
-
- if len(medium) > 3:
- print(f"\n ... and {len(medium) - 3} more medium severity conflicts")
- print()
-
-# Example: How conflicts appear in final skill
-print("=" * 70)
-print("HOW CONFLICTS APPEAR IN SKILL.MD")
-print("=" * 70)
-print()
-
-example_conflict = high[0] if high else medium[0] if medium else conflicts[0]
-
-print("```markdown")
-print("## 🔧 API Reference")
-print()
-print("### ⚠️ APIs with Conflicts")
-print()
-print(f"#### `{example_conflict['api_name']}`")
-print()
-print(f"⚠️ **Conflict**: {example_conflict['difference']}")
-print()
-
-if example_conflict.get('docs_info'):
- print("**Documentation says:**")
- print("```")
- print(example_conflict['docs_info'].get('raw_signature', 'N/A'))
- print("```")
- print()
-
-if example_conflict.get('code_info'):
- print("**Code implementation:**")
- print("```python")
- params = example_conflict['code_info'].get('parameters', [])
- param_strs = []
- for p in params:
- if p['name'] == 'self':
- continue
- param_str = p['name']
- if p.get('type_hint'):
- param_str += f": {p['type_hint']}"
- if p.get('default'):
- param_str += f" = {p['default']}"
- param_strs.append(param_str)
-
- sig = f"def {example_conflict['code_info']['name']}({', '.join(param_strs)})"
- if example_conflict['code_info'].get('return_type'):
- sig += f" -> {example_conflict['code_info']['return_type']}"
-
- print(sig)
- print("```")
-print()
-
-print("*Source: both (conflict)*")
-print("```")
-print()
-
-# Key takeaways
-print("=" * 70)
-print("KEY TAKEAWAYS")
-print("=" * 70)
-print()
-
-print("✅ **What the Unified Scraper Does:**")
-print(" 1. Extracts APIs from both documentation and code")
-print(" 2. Compares them to detect discrepancies")
-print(" 3. Classifies conflicts by type and severity")
-print(" 4. Provides actionable suggestions")
-print(" 5. Shows both versions transparently in the skill")
-print()
-
-print("⚠️ **Common Conflict Types:**")
-print(" - **Missing in docs**: Undocumented features in code")
-print(" - **Missing in code**: Documented but not implemented")
-print(" - **Signature mismatch**: Different parameters/types")
-print(" - **Description mismatch**: Different explanations")
-print()
-
-print("🎯 **Value:**")
-print(" - Identifies documentation gaps")
-print(" - Catches outdated documentation")
-print(" - Highlights implementation differences")
-print(" - Creates single source of truth showing reality")
-print()
-
-print("=" * 70)
-print("END OF DEMO")
-print("=" * 70)
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/example-mcp-config.json b/assets/skills/skills-skills/scripts/Skill_Seekers-development/example-mcp-config.json
deleted file mode 100644
index 80d946c..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/example-mcp-config.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "python3",
- "args": [
- "/mnt/1ece809a-2821-4f10-aecb-fcdf34760c0b/Git/Skill_Seekers/mcp/server.py"
- ],
- "cwd": "/mnt/1ece809a-2821-4f10-aecb-fcdf34760c0b/Git/Skill_Seekers"
- }
- }
-}
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/mypy.ini b/assets/skills/skills-skills/scripts/Skill_Seekers-development/mypy.ini
deleted file mode 100644
index 857c31c..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/mypy.ini
+++ /dev/null
@@ -1,13 +0,0 @@
-[mypy]
-python_version = 3.10
-warn_return_any = False
-warn_unused_configs = True
-disallow_untyped_defs = False
-check_untyped_defs = True
-ignore_missing_imports = True
-no_implicit_optional = True
-show_error_codes = True
-
-# Gradual typing - be lenient for now
-disallow_incomplete_defs = False
-disallow_untyped_calls = False
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/pyproject.toml b/assets/skills/skills-skills/scripts/Skill_Seekers-development/pyproject.toml
deleted file mode 100644
index 91c8391..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/pyproject.toml
+++ /dev/null
@@ -1,149 +0,0 @@
-[build-system]
-requires = ["setuptools>=61.0", "wheel"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "skill-seekers"
-version = "2.1.1"
-description = "Convert documentation websites, GitHub repositories, and PDFs into Claude AI skills"
-readme = "README.md"
-requires-python = ">=3.10"
-license = {text = "MIT"}
-authors = [
- {name = "Yusuf Karaaslan"}
-]
-keywords = [
- "claude",
- "ai",
- "documentation",
- "scraping",
- "skills",
- "llm",
- "mcp",
- "automation"
-]
-classifiers = [
- "Development Status :: 4 - Beta",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
- "Operating System :: OS Independent",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
- "Topic :: Software Development :: Documentation",
- "Topic :: Software Development :: Libraries :: Python Modules",
- "Topic :: Text Processing :: Markup :: Markdown",
-]
-
-# Core dependencies
-dependencies = [
- "requests>=2.32.5",
- "beautifulsoup4>=4.14.2",
- "PyGithub>=2.5.0",
- "mcp>=1.18.0",
- "httpx>=0.28.1",
- "httpx-sse>=0.4.3",
- "PyMuPDF>=1.24.14",
- "Pillow>=11.0.0",
- "pytesseract>=0.3.13",
- "pydantic>=2.12.3",
- "pydantic-settings>=2.11.0",
- "python-dotenv>=1.1.1",
- "jsonschema>=4.25.1",
- "click>=8.3.0",
- "Pygments>=2.19.2",
-]
-
-[project.optional-dependencies]
-# Development dependencies
-dev = [
- "pytest>=8.4.2",
- "pytest-cov>=7.0.0",
- "coverage>=7.11.0",
-]
-
-# MCP server dependencies (included by default, but optional)
-mcp = [
- "mcp>=1.18.0",
- "httpx>=0.28.1",
- "httpx-sse>=0.4.3",
- "uvicorn>=0.38.0",
- "starlette>=0.48.0",
- "sse-starlette>=3.0.2",
-]
-
-# All optional dependencies combined
-all = [
- "pytest>=8.4.2",
- "pytest-cov>=7.0.0",
- "coverage>=7.11.0",
- "mcp>=1.18.0",
- "httpx>=0.28.1",
- "httpx-sse>=0.4.3",
- "uvicorn>=0.38.0",
- "starlette>=0.48.0",
- "sse-starlette>=3.0.2",
-]
-
-[project.urls]
-Homepage = "https://github.com/yusufkaraaslan/Skill_Seekers"
-Repository = "https://github.com/yusufkaraaslan/Skill_Seekers"
-"Bug Tracker" = "https://github.com/yusufkaraaslan/Skill_Seekers/issues"
-Documentation = "https://github.com/yusufkaraaslan/Skill_Seekers#readme"
-
-[project.scripts]
-# Main unified CLI
-skill-seekers = "skill_seekers.cli.main:main"
-
-# Individual tool entry points
-skill-seekers-scrape = "skill_seekers.cli.doc_scraper:main"
-skill-seekers-github = "skill_seekers.cli.github_scraper:main"
-skill-seekers-pdf = "skill_seekers.cli.pdf_scraper:main"
-skill-seekers-unified = "skill_seekers.cli.unified_scraper:main"
-skill-seekers-enhance = "skill_seekers.cli.enhance_skill_local:main"
-skill-seekers-package = "skill_seekers.cli.package_skill:main"
-skill-seekers-upload = "skill_seekers.cli.upload_skill:main"
-skill-seekers-estimate = "skill_seekers.cli.estimate_pages:main"
-
-[tool.setuptools]
-packages = ["skill_seekers", "skill_seekers.cli", "skill_seekers.mcp", "skill_seekers.mcp.tools"]
-
-[tool.setuptools.package-dir]
-"" = "src"
-
-[tool.setuptools.package-data]
-skill_seekers = ["py.typed"]
-
-[tool.pytest.ini_options]
-testpaths = ["tests"]
-python_files = ["test_*.py"]
-python_classes = ["Test*"]
-python_functions = ["test_*"]
-addopts = "-v --tb=short --strict-markers"
-
-[tool.coverage.run]
-source = ["src/skill_seekers"]
-omit = ["*/tests/*", "*/__pycache__/*", "*/venv/*"]
-
-[tool.coverage.report]
-exclude_lines = [
- "pragma: no cover",
- "def __repr__",
- "raise AssertionError",
- "raise NotImplementedError",
- "if __name__ == .__main__.:",
- "if TYPE_CHECKING:",
- "@abstractmethod",
-]
-
-[tool.uv]
-dev-dependencies = [
- "pytest>=8.4.2",
- "pytest-cov>=7.0.0",
- "coverage>=7.11.0",
-]
-
-[tool.uv.sources]
-# Use PyPI for all dependencies
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/requirements.txt b/assets/skills/skills-skills/scripts/Skill_Seekers-development/requirements.txt
deleted file mode 100644
index c6e9ced..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/requirements.txt
+++ /dev/null
@@ -1,42 +0,0 @@
-annotated-types==0.7.0
-anyio==4.11.0
-attrs==25.4.0
-beautifulsoup4==4.14.2
-certifi==2025.10.5
-charset-normalizer==3.4.4
-click==8.3.0
-coverage==7.11.0
-h11==0.16.0
-httpcore==1.0.9
-httpx==0.28.1
-httpx-sse==0.4.3
-idna==3.11
-iniconfig==2.3.0
-jsonschema==4.25.1
-jsonschema-specifications==2025.9.1
-mcp==1.18.0
-packaging==25.0
-pluggy==1.6.0
-pydantic==2.12.3
-pydantic-settings==2.11.0
-pydantic_core==2.41.4
-PyGithub==2.5.0
-Pygments==2.19.2
-PyMuPDF==1.24.14
-Pillow==11.0.0
-pytesseract==0.3.13
-pytest==8.4.2
-pytest-cov==7.0.0
-python-dotenv==1.1.1
-python-multipart==0.0.20
-referencing==0.37.0
-requests==2.32.5
-rpds-py==0.27.1
-sniffio==1.3.1
-soupsieve==2.8
-sse-starlette==3.0.2
-starlette==0.48.0
-typing-inspection==0.4.2
-typing_extensions==4.15.0
-urllib3==2.5.0
-uvicorn==0.38.0
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/setup_mcp.sh b/assets/skills/skills-skills/scripts/Skill_Seekers-development/setup_mcp.sh
deleted file mode 100644
index 4047102..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/setup_mcp.sh
+++ /dev/null
@@ -1,266 +0,0 @@
-#!/bin/bash
-# Skill Seeker MCP Server - Quick Setup Script
-# This script automates the MCP server setup for Claude Code
-
-set -e # Exit on error
-
-echo "=================================================="
-echo "Skill Seeker MCP Server - Quick Setup"
-echo "=================================================="
-echo ""
-
-# Colors for output
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-RED='\033[0;31m'
-NC='\033[0m' # No Color
-
-# Step 1: Check Python version
-echo "Step 1: Checking Python version..."
-if ! command -v python3 &> /dev/null; then
- echo -e "${RED}❌ Error: python3 not found${NC}"
- echo "Please install Python 3.7 or higher"
- exit 1
-fi
-
-PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
-echo -e "${GREEN}✓${NC} Python $PYTHON_VERSION found"
-echo ""
-
-# Step 2: Get repository path
-REPO_PATH=$(pwd)
-echo "Step 2: Repository location"
-echo "Path: $REPO_PATH"
-echo ""
-
-# Step 3: Install dependencies
-echo "Step 3: Installing Python dependencies..."
-
-# Check if we're in a virtual environment
-if [[ -n "$VIRTUAL_ENV" ]]; then
- echo -e "${GREEN}✓${NC} Virtual environment detected: $VIRTUAL_ENV"
- PIP_INSTALL_CMD="pip install"
-elif [[ -d "venv" ]]; then
- echo -e "${YELLOW}⚠${NC} Virtual environment found but not activated"
- echo "Activating venv..."
- source venv/bin/activate
- PIP_INSTALL_CMD="pip install"
-else
- echo -e "${YELLOW}⚠${NC} No virtual environment found"
- echo "It's recommended to use a virtual environment to avoid conflicts."
- echo ""
- read -p "Would you like to create one now? (y/n) " -n 1 -r
- echo ""
-
- if [[ $REPLY =~ ^[Yy]$ ]]; then
- echo "Creating virtual environment..."
- python3 -m venv venv || {
- echo -e "${RED}❌ Failed to create virtual environment${NC}"
- echo "Falling back to system install..."
- PIP_INSTALL_CMD="pip3 install --user --break-system-packages"
- }
-
- if [[ -d "venv" ]]; then
- source venv/bin/activate
- PIP_INSTALL_CMD="pip install"
- echo -e "${GREEN}✓${NC} Virtual environment created and activated"
- fi
- else
- echo "Proceeding with system install (using --user --break-system-packages)..."
- echo -e "${YELLOW}Note:${NC} This may override system-managed packages"
- PIP_INSTALL_CMD="pip3 install --user --break-system-packages"
- fi
-fi
-
-echo "This will install: mcp, requests, beautifulsoup4"
-read -p "Continue? (y/n) " -n 1 -r
-echo ""
-
-if [[ $REPLY =~ ^[Yy]$ ]]; then
- echo "Installing package in editable mode..."
- $PIP_INSTALL_CMD -e . || {
- echo -e "${RED}❌ Failed to install package${NC}"
- exit 1
- }
-
- echo -e "${GREEN}✓${NC} Dependencies installed successfully"
-else
- echo "Skipping dependency installation"
-fi
-echo ""
-
-# Step 4: Test MCP server
-echo "Step 4: Testing MCP server..."
-timeout 3 python3 src/skill_seekers/mcp/server.py 2>/dev/null || {
- if [ $? -eq 124 ]; then
- echo -e "${GREEN}✓${NC} MCP server starts correctly (timeout expected)"
- else
- echo -e "${YELLOW}⚠${NC} MCP server test inconclusive, but may still work"
- fi
-}
-echo ""
-
-# Step 5: Optional - Run tests
-echo "Step 5: Run test suite? (optional)"
-read -p "Run MCP tests to verify everything works? (y/n) " -n 1 -r
-echo ""
-
-if [[ $REPLY =~ ^[Yy]$ ]]; then
- # Check if pytest is installed
- if ! command -v pytest &> /dev/null; then
- echo "Installing pytest..."
- $PIP_INSTALL_CMD pytest || {
- echo -e "${YELLOW}⚠${NC} Could not install pytest, skipping tests"
- }
- fi
-
- if command -v pytest &> /dev/null; then
- echo "Running MCP server tests..."
- python3 -m pytest tests/test_mcp_server.py -v --tb=short || {
- echo -e "${RED}❌ Some tests failed${NC}"
- echo "The server may still work, but please check the errors above"
- }
- fi
-else
- echo "Skipping tests"
-fi
-echo ""
-
-# Step 6: Configure Claude Code
-echo "Step 6: Configure Claude Code"
-echo "=================================================="
-echo ""
-echo "You need to add this configuration to Claude Code:"
-echo ""
-echo -e "${YELLOW}Configuration file:${NC} ~/.config/claude-code/mcp.json"
-echo ""
-echo "Add this JSON configuration (paths are auto-detected for YOUR system):"
-echo ""
-echo -e "${GREEN}{"
-echo " \"mcpServers\": {"
-echo " \"skill-seeker\": {"
-echo " \"command\": \"python3\","
-echo " \"args\": ["
-echo " \"$REPO_PATH/src/skill_seekers/mcp/server.py\""
-echo " ],"
-echo " \"cwd\": \"$REPO_PATH\""
-echo " }"
-echo " }"
-echo -e "}${NC}"
-echo ""
-echo -e "${YELLOW}Note:${NC} The paths above are YOUR actual paths (not placeholders!)"
-echo ""
-
-# Ask if user wants auto-configure
-echo ""
-read -p "Auto-configure Claude Code now? (y/n) " -n 1 -r
-echo ""
-
-if [[ $REPLY =~ ^[Yy]$ ]]; then
- # Check if config already exists
- if [ -f ~/.config/claude-code/mcp.json ]; then
- echo -e "${YELLOW}⚠ Warning: ~/.config/claude-code/mcp.json already exists${NC}"
- echo "Current contents:"
- cat ~/.config/claude-code/mcp.json
- echo ""
- read -p "Overwrite? (y/n) " -n 1 -r
- echo ""
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
- echo "Skipping auto-configuration"
- echo "Please manually add the skill-seeker server to your config"
- exit 0
- fi
- fi
-
- # Create config directory
- mkdir -p ~/.config/claude-code
-
- # Write configuration with actual expanded path
- cat > ~/.config/claude-code/mcp.json << EOF
-{
- "mcpServers": {
- "skill-seeker": {
- "command": "python3",
- "args": [
- "$REPO_PATH/src/skill_seekers/mcp/server.py"
- ],
- "cwd": "$REPO_PATH"
- }
- }
-}
-EOF
-
- echo -e "${GREEN}✓${NC} Configuration written to ~/.config/claude-code/mcp.json"
- echo ""
- echo "Configuration contents:"
- cat ~/.config/claude-code/mcp.json
- echo ""
-
- # Verify the path exists
- if [ -f "$REPO_PATH/src/skill_seekers/mcp/server.py" ]; then
- echo -e "${GREEN}✓${NC} Verified: MCP server file exists at $REPO_PATH/src/skill_seekers/mcp/server.py"
- else
- echo -e "${RED}❌ Warning: MCP server not found at $REPO_PATH/src/skill_seekers/mcp/server.py${NC}"
- echo "Please check the path!"
- fi
-else
- echo "Skipping auto-configuration"
- echo "Please manually configure Claude Code using the JSON above"
- echo ""
- echo "IMPORTANT: Replace \$REPO_PATH with the actual path: $REPO_PATH"
-fi
-echo ""
-
-# Step 7: Test the configuration
-if [ -f ~/.config/claude-code/mcp.json ]; then
- echo "Step 7: Testing MCP configuration..."
- echo "Checking if paths are correct..."
-
- # Extract the configured path
- if command -v jq &> /dev/null; then
- CONFIGURED_PATH=$(jq -r '.mcpServers["skill-seeker"].args[0]' ~/.config/claude-code/mcp.json 2>/dev/null || echo "")
- if [ -n "$CONFIGURED_PATH" ] && [ -f "$CONFIGURED_PATH" ]; then
- echo -e "${GREEN}✓${NC} MCP server path is valid: $CONFIGURED_PATH"
- elif [ -n "$CONFIGURED_PATH" ]; then
- echo -e "${YELLOW}⚠${NC} Warning: Configured path doesn't exist: $CONFIGURED_PATH"
- fi
- else
- echo "Install 'jq' for config validation: brew install jq (macOS) or apt install jq (Linux)"
- fi
-fi
-echo ""
-
-# Step 8: Final instructions
-echo "=================================================="
-echo "Setup Complete!"
-echo "=================================================="
-echo ""
-echo "Next steps:"
-echo ""
-echo " 1. ${YELLOW}Restart Claude Code${NC} (quit and reopen, don't just close window)"
-echo " 2. In Claude Code, test with: ${GREEN}\"List all available configs\"${NC}"
-echo " 3. You should see 9 Skill Seeker tools available"
-echo ""
-echo "Available MCP Tools:"
-echo " • generate_config - Create new config files"
-echo " • estimate_pages - Estimate scraping time"
-echo " • scrape_docs - Scrape documentation"
-echo " • package_skill - Create .zip files"
-echo " • list_configs - Show available configs"
-echo " • validate_config - Validate config files"
-echo ""
-echo "Example commands to try in Claude Code:"
-echo " • ${GREEN}List all available configs${NC}"
-echo " • ${GREEN}Validate configs/react.json${NC}"
-echo " • ${GREEN}Generate config for Tailwind at https://tailwindcss.com/docs${NC}"
-echo ""
-echo "Documentation:"
-echo " • MCP Setup Guide: ${YELLOW}docs/MCP_SETUP.md${NC}"
-echo " • Full docs: ${YELLOW}README.md${NC}"
-echo ""
-echo "Troubleshooting:"
-echo " • Check logs: ~/Library/Logs/Claude Code/ (macOS)"
-echo " • Test server: python3 src/skill_seekers/mcp/server.py"
-echo " • Run tests: python3 -m pytest tests/test_mcp_server.py -v"
-echo ""
-echo "Happy skill creating! 🚀"
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/__init__.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/__init__.py
deleted file mode 100644
index 752904b..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/__init__.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""
-Skill Seekers - Convert documentation, GitHub repos, and PDFs into Claude AI skills.
-
-This package provides tools for automatically scraping, organizing, and packaging
-documentation from various sources into uploadable Claude AI skills.
-"""
-
-__version__ = "2.0.0"
-__author__ = "Yusuf Karaaslan"
-__license__ = "MIT"
-
-# Expose main components for easier imports
-from skill_seekers.cli import __version__ as cli_version
-from skill_seekers.mcp import __version__ as mcp_version
-
-__all__ = [
- "__version__",
- "__author__",
- "__license__",
- "cli_version",
- "mcp_version",
-]
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/__init__.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/__init__.py
deleted file mode 100644
index d782d5d..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/__init__.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""Skill Seekers CLI tools package.
-
-This package provides command-line tools for converting documentation
-websites into Claude AI skills.
-
-Main modules:
- - doc_scraper: Main documentation scraping and skill building tool
- - llms_txt_detector: Detect llms.txt files at documentation URLs
- - llms_txt_downloader: Download llms.txt content
- - llms_txt_parser: Parse llms.txt markdown content
- - pdf_scraper: Extract documentation from PDF files
- - enhance_skill: AI-powered skill enhancement (API-based)
- - enhance_skill_local: AI-powered skill enhancement (local)
- - estimate_pages: Estimate page count before scraping
- - package_skill: Package skills into .zip files
- - upload_skill: Upload skills to Claude
- - utils: Shared utility functions
-"""
-
-from .llms_txt_detector import LlmsTxtDetector
-from .llms_txt_downloader import LlmsTxtDownloader
-from .llms_txt_parser import LlmsTxtParser
-
-try:
- from .utils import open_folder, read_reference_files
-except ImportError:
- # utils.py might not exist in all configurations
- open_folder = None
- read_reference_files = None
-
-__version__ = "2.0.0"
-
-__all__ = [
- "LlmsTxtDetector",
- "LlmsTxtDownloader",
- "LlmsTxtParser",
- "open_folder",
- "read_reference_files",
-]
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/code_analyzer.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/code_analyzer.py
deleted file mode 100644
index cf33b16..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/code_analyzer.py
+++ /dev/null
@@ -1,500 +0,0 @@
-#!/usr/bin/env python3
-"""
-Code Analyzer for GitHub Repositories
-
-Extracts code signatures at configurable depth levels:
-- surface: File tree only (existing behavior)
-- deep: Parse files for signatures, parameters, types
-- full: Complete AST analysis (future enhancement)
-
-Supports multiple languages with language-specific parsers.
-"""
-
-import ast
-import re
-import logging
-from typing import Dict, List, Any, Optional
-from dataclasses import dataclass, asdict
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class Parameter:
- """Represents a function parameter."""
- name: str
- type_hint: Optional[str] = None
- default: Optional[str] = None
-
-
-@dataclass
-class FunctionSignature:
- """Represents a function/method signature."""
- name: str
- parameters: List[Parameter]
- return_type: Optional[str] = None
- docstring: Optional[str] = None
- line_number: Optional[int] = None
- is_async: bool = False
- is_method: bool = False
- decorators: List[str] = None
-
- def __post_init__(self):
- if self.decorators is None:
- self.decorators = []
-
-
-@dataclass
-class ClassSignature:
- """Represents a class signature."""
- name: str
- base_classes: List[str]
- methods: List[FunctionSignature]
- docstring: Optional[str] = None
- line_number: Optional[int] = None
-
-
-class CodeAnalyzer:
- """
- Analyzes code at different depth levels.
- """
-
- def __init__(self, depth: str = 'surface'):
- """
- Initialize code analyzer.
-
- Args:
- depth: Analysis depth ('surface', 'deep', 'full')
- """
- self.depth = depth
-
- def analyze_file(self, file_path: str, content: str, language: str) -> Dict[str, Any]:
- """
- Analyze a single file based on depth level.
-
- Args:
- file_path: Path to file in repository
- content: File content as string
- language: Programming language (Python, JavaScript, etc.)
-
- Returns:
- Dict containing extracted signatures
- """
- if self.depth == 'surface':
- return {} # Surface level doesn't analyze individual files
-
- logger.debug(f"Analyzing {file_path} (language: {language}, depth: {self.depth})")
-
- try:
- if language == 'Python':
- return self._analyze_python(content, file_path)
- elif language in ['JavaScript', 'TypeScript']:
- return self._analyze_javascript(content, file_path)
- elif language in ['C', 'C++']:
- return self._analyze_cpp(content, file_path)
- else:
- logger.debug(f"No analyzer for language: {language}")
- return {}
- except Exception as e:
- logger.warning(f"Error analyzing {file_path}: {e}")
- return {}
-
- def _analyze_python(self, content: str, file_path: str) -> Dict[str, Any]:
- """Analyze Python file using AST."""
- try:
- tree = ast.parse(content)
- except SyntaxError as e:
- logger.debug(f"Syntax error in {file_path}: {e}")
- return {}
-
- classes = []
- functions = []
-
- for node in ast.walk(tree):
- if isinstance(node, ast.ClassDef):
- class_sig = self._extract_python_class(node)
- classes.append(asdict(class_sig))
- elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
- # Only top-level functions (not methods)
- # Fix AST parser to check isinstance(parent.body, list) before 'in' operator
- is_method = False
- try:
- is_method = any(isinstance(parent, ast.ClassDef)
- for parent in ast.walk(tree)
- if hasattr(parent, 'body') and isinstance(parent.body, list) and node in parent.body)
- except (TypeError, AttributeError):
- # If body is not iterable or check fails, assume it's a top-level function
- is_method = False
-
- if not is_method:
- func_sig = self._extract_python_function(node)
- functions.append(asdict(func_sig))
-
- return {
- 'classes': classes,
- 'functions': functions
- }
-
- def _extract_python_class(self, node: ast.ClassDef) -> ClassSignature:
- """Extract class signature from AST node."""
- # Extract base classes
- bases = []
- for base in node.bases:
- if isinstance(base, ast.Name):
- bases.append(base.id)
- elif isinstance(base, ast.Attribute):
- bases.append(f"{base.value.id}.{base.attr}" if hasattr(base.value, 'id') else base.attr)
-
- # Extract methods
- methods = []
- for item in node.body:
- if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
- method_sig = self._extract_python_function(item, is_method=True)
- methods.append(method_sig)
-
- # Extract docstring
- docstring = ast.get_docstring(node)
-
- return ClassSignature(
- name=node.name,
- base_classes=bases,
- methods=methods,
- docstring=docstring,
- line_number=node.lineno
- )
-
- def _extract_python_function(self, node, is_method: bool = False) -> FunctionSignature:
- """Extract function signature from AST node."""
- # Extract parameters
- params = []
- for arg in node.args.args:
- param_type = None
- if arg.annotation:
- param_type = ast.unparse(arg.annotation) if hasattr(ast, 'unparse') else None
-
- params.append(Parameter(
- name=arg.arg,
- type_hint=param_type
- ))
-
- # Extract defaults
- defaults = node.args.defaults
- if defaults:
- # Defaults are aligned to the end of params
- num_no_default = len(params) - len(defaults)
- for i, default in enumerate(defaults):
- param_idx = num_no_default + i
- if param_idx < len(params):
- try:
- params[param_idx].default = ast.unparse(default) if hasattr(ast, 'unparse') else str(default)
- except:
- params[param_idx].default = "..."
-
- # Extract return type
- return_type = None
- if node.returns:
- try:
- return_type = ast.unparse(node.returns) if hasattr(ast, 'unparse') else None
- except:
- pass
-
- # Extract decorators
- decorators = []
- for decorator in node.decorator_list:
- try:
- if hasattr(ast, 'unparse'):
- decorators.append(ast.unparse(decorator))
- elif isinstance(decorator, ast.Name):
- decorators.append(decorator.id)
- except:
- pass
-
- # Extract docstring
- docstring = ast.get_docstring(node)
-
- return FunctionSignature(
- name=node.name,
- parameters=params,
- return_type=return_type,
- docstring=docstring,
- line_number=node.lineno,
- is_async=isinstance(node, ast.AsyncFunctionDef),
- is_method=is_method,
- decorators=decorators
- )
-
- def _analyze_javascript(self, content: str, file_path: str) -> Dict[str, Any]:
- """
- Analyze JavaScript/TypeScript file using regex patterns.
-
- Note: This is a simplified approach. For production, consider using
- a proper JS/TS parser like esprima or ts-morph.
- """
- classes = []
- functions = []
-
- # Extract class definitions
- class_pattern = r'class\s+(\w+)(?:\s+extends\s+(\w+))?\s*\{'
- for match in re.finditer(class_pattern, content):
- class_name = match.group(1)
- base_class = match.group(2) if match.group(2) else None
-
- # Try to extract methods (simplified)
- class_block_start = match.end()
- # This is a simplification - proper parsing would track braces
- class_block_end = content.find('}', class_block_start)
- if class_block_end != -1:
- class_body = content[class_block_start:class_block_end]
- methods = self._extract_js_methods(class_body)
- else:
- methods = []
-
- classes.append({
- 'name': class_name,
- 'base_classes': [base_class] if base_class else [],
- 'methods': methods,
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1
- })
-
- # Extract top-level functions
- func_pattern = r'(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)'
- for match in re.finditer(func_pattern, content):
- func_name = match.group(1)
- params_str = match.group(2)
- is_async = 'async' in match.group(0)
-
- params = self._parse_js_parameters(params_str)
-
- functions.append({
- 'name': func_name,
- 'parameters': params,
- 'return_type': None, # JS doesn't have type annotations (unless TS)
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1,
- 'is_async': is_async,
- 'is_method': False,
- 'decorators': []
- })
-
- # Extract arrow functions assigned to const/let
- arrow_pattern = r'(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>'
- for match in re.finditer(arrow_pattern, content):
- func_name = match.group(1)
- params_str = match.group(2)
- is_async = 'async' in match.group(0)
-
- params = self._parse_js_parameters(params_str)
-
- functions.append({
- 'name': func_name,
- 'parameters': params,
- 'return_type': None,
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1,
- 'is_async': is_async,
- 'is_method': False,
- 'decorators': []
- })
-
- return {
- 'classes': classes,
- 'functions': functions
- }
-
- def _extract_js_methods(self, class_body: str) -> List[Dict]:
- """Extract method signatures from class body."""
- methods = []
-
- # Match method definitions
- method_pattern = r'(?:async\s+)?(\w+)\s*\(([^)]*)\)'
- for match in re.finditer(method_pattern, class_body):
- method_name = match.group(1)
- params_str = match.group(2)
- is_async = 'async' in match.group(0)
-
- # Skip constructor keyword detection
- if method_name in ['if', 'for', 'while', 'switch']:
- continue
-
- params = self._parse_js_parameters(params_str)
-
- methods.append({
- 'name': method_name,
- 'parameters': params,
- 'return_type': None,
- 'docstring': None,
- 'line_number': None,
- 'is_async': is_async,
- 'is_method': True,
- 'decorators': []
- })
-
- return methods
-
- def _parse_js_parameters(self, params_str: str) -> List[Dict]:
- """Parse JavaScript parameter string."""
- params = []
-
- if not params_str.strip():
- return params
-
- # Split by comma (simplified - doesn't handle complex default values)
- param_list = [p.strip() for p in params_str.split(',')]
-
- for param in param_list:
- if not param:
- continue
-
- # Check for default value
- if '=' in param:
- name, default = param.split('=', 1)
- name = name.strip()
- default = default.strip()
- else:
- name = param
- default = None
-
- # Check for type annotation (TypeScript)
- type_hint = None
- if ':' in name:
- name, type_hint = name.split(':', 1)
- name = name.strip()
- type_hint = type_hint.strip()
-
- params.append({
- 'name': name,
- 'type_hint': type_hint,
- 'default': default
- })
-
- return params
-
- def _analyze_cpp(self, content: str, file_path: str) -> Dict[str, Any]:
- """
- Analyze C/C++ header file using regex patterns.
-
- Note: This is a simplified approach focusing on header files.
- For production, consider using libclang or similar.
- """
- classes = []
- functions = []
-
- # Extract class definitions (simplified - doesn't handle nested classes)
- class_pattern = r'class\s+(\w+)(?:\s*:\s*public\s+(\w+))?\s*\{'
- for match in re.finditer(class_pattern, content):
- class_name = match.group(1)
- base_class = match.group(2) if match.group(2) else None
-
- classes.append({
- 'name': class_name,
- 'base_classes': [base_class] if base_class else [],
- 'methods': [], # Simplified - would need to parse class body
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1
- })
-
- # Extract function declarations
- func_pattern = r'(\w+(?:\s*\*|\s*&)?)\s+(\w+)\s*\(([^)]*)\)'
- for match in re.finditer(func_pattern, content):
- return_type = match.group(1).strip()
- func_name = match.group(2)
- params_str = match.group(3)
-
- # Skip common keywords
- if func_name in ['if', 'for', 'while', 'switch', 'return']:
- continue
-
- params = self._parse_cpp_parameters(params_str)
-
- functions.append({
- 'name': func_name,
- 'parameters': params,
- 'return_type': return_type,
- 'docstring': None,
- 'line_number': content[:match.start()].count('\n') + 1,
- 'is_async': False,
- 'is_method': False,
- 'decorators': []
- })
-
- return {
- 'classes': classes,
- 'functions': functions
- }
-
- def _parse_cpp_parameters(self, params_str: str) -> List[Dict]:
- """Parse C++ parameter string."""
- params = []
-
- if not params_str.strip() or params_str.strip() == 'void':
- return params
-
- # Split by comma (simplified)
- param_list = [p.strip() for p in params_str.split(',')]
-
- for param in param_list:
- if not param:
- continue
-
- # Check for default value
- default = None
- if '=' in param:
- param, default = param.rsplit('=', 1)
- param = param.strip()
- default = default.strip()
-
- # Extract type and name (simplified)
- # Format: "type name" or "type* name" or "type& name"
- parts = param.split()
- if len(parts) >= 2:
- param_type = ' '.join(parts[:-1])
- param_name = parts[-1]
- else:
- param_type = param
- param_name = "unknown"
-
- params.append({
- 'name': param_name,
- 'type_hint': param_type,
- 'default': default
- })
-
- return params
-
-
-if __name__ == '__main__':
- # Test the analyzer
- python_code = '''
-class Node2D:
- """Base class for 2D nodes."""
-
- def move_local_x(self, delta: float, snap: bool = False) -> None:
- """Move node along local X axis."""
- pass
-
- async def tween_position(self, target: tuple, duration: float = 1.0):
- """Animate position to target."""
- pass
-
-def create_sprite(texture: str) -> Node2D:
- """Create a new sprite node."""
- return Node2D()
-'''
-
- analyzer = CodeAnalyzer(depth='deep')
- result = analyzer.analyze_file('test.py', python_code, 'Python')
-
- print("Analysis Result:")
- print(f"Classes: {len(result.get('classes', []))}")
- print(f"Functions: {len(result.get('functions', []))}")
-
- if result.get('classes'):
- cls = result['classes'][0]
- print(f"\nClass: {cls['name']}")
- print(f" Methods: {len(cls['methods'])}")
- for method in cls['methods']:
- params = ', '.join([f"{p['name']}: {p['type_hint']}" + (f" = {p['default']}" if p.get('default') else "")
- for p in method['parameters']])
- print(f" {method['name']}({params}) -> {method['return_type']}")
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/config_validator.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/config_validator.py
deleted file mode 100644
index b8391de..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/config_validator.py
+++ /dev/null
@@ -1,376 +0,0 @@
-#!/usr/bin/env python3
-"""
-Unified Config Validator
-
-Validates unified config format that supports multiple sources:
-- documentation (website scraping)
-- github (repository scraping)
-- pdf (PDF document scraping)
-
-Also provides backward compatibility detection for legacy configs.
-"""
-
-import json
-import logging
-from typing import Dict, Any, List, Optional, Union
-from pathlib import Path
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-class ConfigValidator:
- """
- Validates unified config format and provides backward compatibility.
- """
-
- # Valid source types
- VALID_SOURCE_TYPES = {'documentation', 'github', 'pdf'}
-
- # Valid merge modes
- VALID_MERGE_MODES = {'rule-based', 'claude-enhanced'}
-
- # Valid code analysis depth levels
- VALID_DEPTH_LEVELS = {'surface', 'deep', 'full'}
-
- def __init__(self, config_or_path: Union[Dict[str, Any], str]):
- """
- Initialize validator with config dict or file path.
-
- Args:
- config_or_path: Either a config dict or path to config JSON file
- """
- if isinstance(config_or_path, dict):
- self.config_path = None
- self.config = config_or_path
- else:
- self.config_path = config_or_path
- self.config = self._load_config()
- self.is_unified = self._detect_format()
-
- def _load_config(self) -> Dict[str, Any]:
- """Load JSON config file."""
- try:
- with open(self.config_path, 'r', encoding='utf-8') as f:
- return json.load(f)
- except FileNotFoundError:
- raise ValueError(f"Config file not found: {self.config_path}")
- except json.JSONDecodeError as e:
- raise ValueError(f"Invalid JSON in config file: {e}")
-
- def _detect_format(self) -> bool:
- """
- Detect if config is unified format or legacy.
-
- Returns:
- True if unified format (has 'sources' array)
- False if legacy format
- """
- return 'sources' in self.config and isinstance(self.config['sources'], list)
-
- def validate(self) -> bool:
- """
- Validate config based on detected format.
-
- Returns:
- True if valid
-
- Raises:
- ValueError if invalid with detailed error message
- """
- if self.is_unified:
- return self._validate_unified()
- else:
- return self._validate_legacy()
-
- def _validate_unified(self) -> bool:
- """Validate unified config format."""
- logger.info("Validating unified config format...")
-
- # Required top-level fields
- if 'name' not in self.config:
- raise ValueError("Missing required field: 'name'")
-
- if 'description' not in self.config:
- raise ValueError("Missing required field: 'description'")
-
- if 'sources' not in self.config:
- raise ValueError("Missing required field: 'sources'")
-
- # Validate sources array
- sources = self.config['sources']
-
- if not isinstance(sources, list):
- raise ValueError("'sources' must be an array")
-
- if len(sources) == 0:
- raise ValueError("'sources' array cannot be empty")
-
- # Validate merge_mode (optional)
- merge_mode = self.config.get('merge_mode', 'rule-based')
- if merge_mode not in self.VALID_MERGE_MODES:
- raise ValueError(f"Invalid merge_mode: '{merge_mode}'. Must be one of {self.VALID_MERGE_MODES}")
-
- # Validate each source
- for i, source in enumerate(sources):
- self._validate_source(source, i)
-
- logger.info(f"✅ Unified config valid: {len(sources)} sources")
- return True
-
- def _validate_source(self, source: Dict[str, Any], index: int):
- """Validate individual source configuration."""
- # Check source has 'type' field
- if 'type' not in source:
- raise ValueError(f"Source {index}: Missing required field 'type'")
-
- source_type = source['type']
-
- if source_type not in self.VALID_SOURCE_TYPES:
- raise ValueError(
- f"Source {index}: Invalid type '{source_type}'. "
- f"Must be one of {self.VALID_SOURCE_TYPES}"
- )
-
- # Type-specific validation
- if source_type == 'documentation':
- self._validate_documentation_source(source, index)
- elif source_type == 'github':
- self._validate_github_source(source, index)
- elif source_type == 'pdf':
- self._validate_pdf_source(source, index)
-
- def _validate_documentation_source(self, source: Dict[str, Any], index: int):
- """Validate documentation source configuration."""
- if 'base_url' not in source:
- raise ValueError(f"Source {index} (documentation): Missing required field 'base_url'")
-
- # Optional but recommended fields
- if 'selectors' not in source:
- logger.warning(f"Source {index} (documentation): No 'selectors' specified, using defaults")
-
- if 'max_pages' in source and not isinstance(source['max_pages'], int):
- raise ValueError(f"Source {index} (documentation): 'max_pages' must be an integer")
-
- def _validate_github_source(self, source: Dict[str, Any], index: int):
- """Validate GitHub source configuration."""
- if 'repo' not in source:
- raise ValueError(f"Source {index} (github): Missing required field 'repo'")
-
- # Validate repo format (owner/repo)
- repo = source['repo']
- if '/' not in repo:
- raise ValueError(
- f"Source {index} (github): Invalid repo format '{repo}'. "
- f"Must be 'owner/repo' (e.g., 'facebook/react')"
- )
-
- # Validate code_analysis_depth if specified
- if 'code_analysis_depth' in source:
- depth = source['code_analysis_depth']
- if depth not in self.VALID_DEPTH_LEVELS:
- raise ValueError(
- f"Source {index} (github): Invalid code_analysis_depth '{depth}'. "
- f"Must be one of {self.VALID_DEPTH_LEVELS}"
- )
-
- # Validate max_issues if specified
- if 'max_issues' in source and not isinstance(source['max_issues'], int):
- raise ValueError(f"Source {index} (github): 'max_issues' must be an integer")
-
- def _validate_pdf_source(self, source: Dict[str, Any], index: int):
- """Validate PDF source configuration."""
- if 'path' not in source:
- raise ValueError(f"Source {index} (pdf): Missing required field 'path'")
-
- # Check if file exists
- pdf_path = source['path']
- if not Path(pdf_path).exists():
- logger.warning(f"Source {index} (pdf): File not found: {pdf_path}")
-
- def _validate_legacy(self) -> bool:
- """
- Validate legacy config format (backward compatibility).
-
- Legacy configs are the old format used by doc_scraper, github_scraper, pdf_scraper.
- """
- logger.info("Detected legacy config format (backward compatible)")
-
- # Detect which legacy type based on fields
- if 'base_url' in self.config:
- logger.info("Legacy type: documentation")
- elif 'repo' in self.config:
- logger.info("Legacy type: github")
- elif 'pdf' in self.config or 'path' in self.config:
- logger.info("Legacy type: pdf")
- else:
- raise ValueError("Cannot detect legacy config type (missing base_url, repo, or pdf)")
-
- return True
-
- def convert_legacy_to_unified(self) -> Dict[str, Any]:
- """
- Convert legacy config to unified format.
-
- Returns:
- Unified config dict
- """
- if self.is_unified:
- logger.info("Config already in unified format")
- return self.config
-
- logger.info("Converting legacy config to unified format...")
-
- # Detect legacy type and convert
- if 'base_url' in self.config:
- return self._convert_legacy_documentation()
- elif 'repo' in self.config:
- return self._convert_legacy_github()
- elif 'pdf' in self.config or 'path' in self.config:
- return self._convert_legacy_pdf()
- else:
- raise ValueError("Cannot convert: unknown legacy format")
-
- def _convert_legacy_documentation(self) -> Dict[str, Any]:
- """Convert legacy documentation config to unified."""
- unified = {
- 'name': self.config.get('name', 'unnamed'),
- 'description': self.config.get('description', 'Documentation skill'),
- 'merge_mode': 'rule-based',
- 'sources': [
- {
- 'type': 'documentation',
- **{k: v for k, v in self.config.items()
- if k not in ['name', 'description']}
- }
- ]
- }
- return unified
-
- def _convert_legacy_github(self) -> Dict[str, Any]:
- """Convert legacy GitHub config to unified."""
- unified = {
- 'name': self.config.get('name', 'unnamed'),
- 'description': self.config.get('description', 'GitHub repository skill'),
- 'merge_mode': 'rule-based',
- 'sources': [
- {
- 'type': 'github',
- **{k: v for k, v in self.config.items()
- if k not in ['name', 'description']}
- }
- ]
- }
- return unified
-
- def _convert_legacy_pdf(self) -> Dict[str, Any]:
- """Convert legacy PDF config to unified."""
- unified = {
- 'name': self.config.get('name', 'unnamed'),
- 'description': self.config.get('description', 'PDF document skill'),
- 'merge_mode': 'rule-based',
- 'sources': [
- {
- 'type': 'pdf',
- **{k: v for k, v in self.config.items()
- if k not in ['name', 'description']}
- }
- ]
- }
- return unified
-
- def get_sources_by_type(self, source_type: str) -> List[Dict[str, Any]]:
- """
- Get all sources of a specific type.
-
- Args:
- source_type: 'documentation', 'github', or 'pdf'
-
- Returns:
- List of sources matching the type
- """
- if not self.is_unified:
- # For legacy, convert and get sources
- unified = self.convert_legacy_to_unified()
- sources = unified['sources']
- else:
- sources = self.config['sources']
-
- return [s for s in sources if s.get('type') == source_type]
-
- def has_multiple_sources(self) -> bool:
- """Check if config has multiple sources (requires merging)."""
- if not self.is_unified:
- return False
- return len(self.config['sources']) > 1
-
- def needs_api_merge(self) -> bool:
- """
- Check if config needs API merging.
-
- Returns True if both documentation and github sources exist
- with API extraction enabled.
- """
- if not self.has_multiple_sources():
- return False
-
- has_docs_api = any(
- s.get('type') == 'documentation' and s.get('extract_api', True)
- for s in self.config['sources']
- )
-
- has_github_code = any(
- s.get('type') == 'github' and s.get('include_code', False)
- for s in self.config['sources']
- )
-
- return has_docs_api and has_github_code
-
-
-def validate_config(config_path: str) -> ConfigValidator:
- """
- Validate config file and return validator instance.
-
- Args:
- config_path: Path to config JSON file
-
- Returns:
- ConfigValidator instance
-
- Raises:
- ValueError if config is invalid
- """
- validator = ConfigValidator(config_path)
- validator.validate()
- return validator
-
-
-if __name__ == '__main__':
- import sys
-
- if len(sys.argv) < 2:
- print("Usage: python config_validator.py ")
- sys.exit(1)
-
- config_file = sys.argv[1]
-
- try:
- validator = validate_config(config_file)
-
- print(f"\n✅ Config valid!")
- print(f" Format: {'Unified' if validator.is_unified else 'Legacy'}")
- print(f" Name: {validator.config.get('name')}")
-
- if validator.is_unified:
- sources = validator.config['sources']
- print(f" Sources: {len(sources)}")
- for i, source in enumerate(sources):
- print(f" {i+1}. {source['type']}")
-
- if validator.needs_api_merge():
- merge_mode = validator.config.get('merge_mode', 'rule-based')
- print(f" ⚠️ API merge required (mode: {merge_mode})")
-
- except ValueError as e:
- print(f"\n❌ Config invalid: {e}")
- sys.exit(1)
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/conflict_detector.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/conflict_detector.py
deleted file mode 100644
index 5f7d4c2..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/conflict_detector.py
+++ /dev/null
@@ -1,513 +0,0 @@
-#!/usr/bin/env python3
-"""
-Conflict Detector for Multi-Source Skills
-
-Detects conflicts between documentation and code:
-- missing_in_docs: API exists in code but not documented
-- missing_in_code: API documented but doesn't exist in code
-- signature_mismatch: Different parameters/types between docs and code
-- description_mismatch: Docs say one thing, code comments say another
-
-Used by unified scraper to identify discrepancies before merging.
-"""
-
-import json
-import logging
-from typing import Dict, List, Any, Optional, Tuple
-from dataclasses import dataclass, asdict
-from difflib import SequenceMatcher
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class Conflict:
- """Represents a conflict between documentation and code."""
- type: str # 'missing_in_docs', 'missing_in_code', 'signature_mismatch', 'description_mismatch'
- severity: str # 'low', 'medium', 'high'
- api_name: str
- docs_info: Optional[Dict[str, Any]] = None
- code_info: Optional[Dict[str, Any]] = None
- difference: Optional[str] = None
- suggestion: Optional[str] = None
-
-
-class ConflictDetector:
- """
- Detects conflicts between documentation and code sources.
- """
-
- def __init__(self, docs_data: Dict[str, Any], github_data: Dict[str, Any]):
- """
- Initialize conflict detector.
-
- Args:
- docs_data: Data from documentation scraper
- github_data: Data from GitHub scraper with code analysis
- """
- self.docs_data = docs_data
- self.github_data = github_data
-
- # Extract API information from both sources
- self.docs_apis = self._extract_docs_apis()
- self.code_apis = self._extract_code_apis()
-
- logger.info(f"Loaded {len(self.docs_apis)} APIs from documentation")
- logger.info(f"Loaded {len(self.code_apis)} APIs from code")
-
- def _extract_docs_apis(self) -> Dict[str, Dict[str, Any]]:
- """
- Extract API information from documentation data.
-
- Returns:
- Dict mapping API name to API info
- """
- apis = {}
-
- # Documentation structure varies, but typically has 'pages' or 'references'
- pages = self.docs_data.get('pages', {})
-
- # Handle both dict and list formats
- if isinstance(pages, dict):
- # Format: {url: page_data, ...}
- for url, page_data in pages.items():
- content = page_data.get('content', '')
- title = page_data.get('title', '')
-
- # Simple heuristic: if title or URL contains "api", "reference", "class", "function"
- # it might be an API page
- if any(keyword in title.lower() or keyword in url.lower()
- for keyword in ['api', 'reference', 'class', 'function', 'method']):
-
- # Extract API signatures from content (simplified)
- extracted_apis = self._parse_doc_content_for_apis(content, url)
- apis.update(extracted_apis)
- elif isinstance(pages, list):
- # Format: [{url: '...', apis: [...]}, ...]
- for page in pages:
- url = page.get('url', '')
- page_apis = page.get('apis', [])
-
- # If APIs are already extracted in the page data
- for api in page_apis:
- api_name = api.get('name', '')
- if api_name:
- apis[api_name] = {
- 'parameters': api.get('parameters', []),
- 'return_type': api.get('return_type', 'Any'),
- 'source_url': url
- }
-
- return apis
-
- def _parse_doc_content_for_apis(self, content: str, source_url: str) -> Dict[str, Dict]:
- """
- Parse documentation content to extract API signatures.
-
- This is a simplified approach - real implementation would need
- to understand the documentation format (Sphinx, JSDoc, etc.)
- """
- apis = {}
-
- # Look for function/method signatures in code blocks
- # Common patterns:
- # - function_name(param1, param2)
- # - ClassName.method_name(param1, param2)
- # - def function_name(param1: type, param2: type) -> return_type
-
- import re
-
- # Pattern for common API signatures
- patterns = [
- # Python style: def name(params) -> return
- r'def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?',
- # JavaScript style: function name(params)
- r'function\s+(\w+)\s*\(([^)]*)\)',
- # C++ style: return_type name(params)
- r'(\w+)\s+(\w+)\s*\(([^)]*)\)',
- # Method style: ClassName.method_name(params)
- r'(\w+)\.(\w+)\s*\(([^)]*)\)'
- ]
-
- for pattern in patterns:
- for match in re.finditer(pattern, content):
- groups = match.groups()
-
- # Parse based on pattern matched
- if 'def' in pattern:
- # Python function
- name = groups[0]
- params_str = groups[1]
- return_type = groups[2] if len(groups) > 2 else None
- elif 'function' in pattern:
- # JavaScript function
- name = groups[0]
- params_str = groups[1]
- return_type = None
- elif '.' in pattern:
- # Class method
- class_name = groups[0]
- method_name = groups[1]
- name = f"{class_name}.{method_name}"
- params_str = groups[2] if len(groups) > 2 else groups[1]
- return_type = None
- else:
- # C++ function
- return_type = groups[0]
- name = groups[1]
- params_str = groups[2]
-
- # Parse parameters
- params = self._parse_param_string(params_str)
-
- apis[name] = {
- 'name': name,
- 'parameters': params,
- 'return_type': return_type,
- 'source': source_url,
- 'raw_signature': match.group(0)
- }
-
- return apis
-
- def _parse_param_string(self, params_str: str) -> List[Dict]:
- """Parse parameter string into list of parameter dicts."""
- if not params_str.strip():
- return []
-
- params = []
- for param in params_str.split(','):
- param = param.strip()
- if not param:
- continue
-
- # Try to extract name and type
- param_info = {'name': param, 'type': None, 'default': None}
-
- # Check for type annotation (: type)
- if ':' in param:
- parts = param.split(':', 1)
- param_info['name'] = parts[0].strip()
- type_part = parts[1].strip()
-
- # Check for default value (= value)
- if '=' in type_part:
- type_str, default_str = type_part.split('=', 1)
- param_info['type'] = type_str.strip()
- param_info['default'] = default_str.strip()
- else:
- param_info['type'] = type_part
-
- # Check for default without type (= value)
- elif '=' in param:
- parts = param.split('=', 1)
- param_info['name'] = parts[0].strip()
- param_info['default'] = parts[1].strip()
-
- params.append(param_info)
-
- return params
-
- def _extract_code_apis(self) -> Dict[str, Dict[str, Any]]:
- """
- Extract API information from GitHub code analysis.
-
- Returns:
- Dict mapping API name to API info
- """
- apis = {}
-
- code_analysis = self.github_data.get('code_analysis', {})
- if not code_analysis:
- return apis
-
- # Support both 'files' and 'analyzed_files' keys
- files = code_analysis.get('files', code_analysis.get('analyzed_files', []))
-
- for file_info in files:
- file_path = file_info.get('file', 'unknown')
-
- # Extract classes and their methods
- for class_info in file_info.get('classes', []):
- class_name = class_info['name']
-
- # Add class itself
- apis[class_name] = {
- 'name': class_name,
- 'type': 'class',
- 'source': file_path,
- 'line': class_info.get('line_number'),
- 'base_classes': class_info.get('base_classes', []),
- 'docstring': class_info.get('docstring')
- }
-
- # Add methods
- for method in class_info.get('methods', []):
- method_name = f"{class_name}.{method['name']}"
- apis[method_name] = {
- 'name': method_name,
- 'type': 'method',
- 'parameters': method.get('parameters', []),
- 'return_type': method.get('return_type'),
- 'source': file_path,
- 'line': method.get('line_number'),
- 'docstring': method.get('docstring'),
- 'is_async': method.get('is_async', False)
- }
-
- # Extract standalone functions
- for func_info in file_info.get('functions', []):
- func_name = func_info['name']
- apis[func_name] = {
- 'name': func_name,
- 'type': 'function',
- 'parameters': func_info.get('parameters', []),
- 'return_type': func_info.get('return_type'),
- 'source': file_path,
- 'line': func_info.get('line_number'),
- 'docstring': func_info.get('docstring'),
- 'is_async': func_info.get('is_async', False)
- }
-
- return apis
-
- def detect_all_conflicts(self) -> List[Conflict]:
- """
- Detect all types of conflicts.
-
- Returns:
- List of Conflict objects
- """
- logger.info("Detecting conflicts between documentation and code...")
-
- conflicts = []
-
- # 1. Find APIs missing in documentation
- conflicts.extend(self._find_missing_in_docs())
-
- # 2. Find APIs missing in code
- conflicts.extend(self._find_missing_in_code())
-
- # 3. Find signature mismatches
- conflicts.extend(self._find_signature_mismatches())
-
- logger.info(f"Found {len(conflicts)} conflicts total")
-
- return conflicts
-
- def _find_missing_in_docs(self) -> List[Conflict]:
- """Find APIs that exist in code but not in documentation."""
- conflicts = []
-
- for api_name, code_info in self.code_apis.items():
- # Simple name matching (can be enhanced with fuzzy matching)
- if api_name not in self.docs_apis:
- # Check if it's a private/internal API (often not documented)
- is_private = api_name.startswith('_') or '__' in api_name
- severity = 'low' if is_private else 'medium'
-
- conflicts.append(Conflict(
- type='missing_in_docs',
- severity=severity,
- api_name=api_name,
- code_info=code_info,
- difference=f"API exists in code ({code_info['source']}) but not found in documentation",
- suggestion="Add documentation for this API" if not is_private else "Consider if this internal API should be documented"
- ))
-
- logger.info(f"Found {len(conflicts)} APIs missing in documentation")
- return conflicts
-
- def _find_missing_in_code(self) -> List[Conflict]:
- """Find APIs that are documented but don't exist in code."""
- conflicts = []
-
- for api_name, docs_info in self.docs_apis.items():
- if api_name not in self.code_apis:
- conflicts.append(Conflict(
- type='missing_in_code',
- severity='high', # This is serious - documented but doesn't exist
- api_name=api_name,
- docs_info=docs_info,
- difference=f"API documented ({docs_info.get('source', 'unknown')}) but not found in code",
- suggestion="Update documentation to remove this API, or add it to codebase"
- ))
-
- logger.info(f"Found {len(conflicts)} APIs missing in code")
- return conflicts
-
- def _find_signature_mismatches(self) -> List[Conflict]:
- """Find APIs where signature differs between docs and code."""
- conflicts = []
-
- # Find APIs that exist in both
- common_apis = set(self.docs_apis.keys()) & set(self.code_apis.keys())
-
- for api_name in common_apis:
- docs_info = self.docs_apis[api_name]
- code_info = self.code_apis[api_name]
-
- # Compare signatures
- mismatch = self._compare_signatures(docs_info, code_info)
-
- if mismatch:
- conflicts.append(Conflict(
- type='signature_mismatch',
- severity=mismatch['severity'],
- api_name=api_name,
- docs_info=docs_info,
- code_info=code_info,
- difference=mismatch['difference'],
- suggestion=mismatch['suggestion']
- ))
-
- logger.info(f"Found {len(conflicts)} signature mismatches")
- return conflicts
-
- def _compare_signatures(self, docs_info: Dict, code_info: Dict) -> Optional[Dict]:
- """
- Compare signatures between docs and code.
-
- Returns:
- Dict with mismatch details if conflict found, None otherwise
- """
- docs_params = docs_info.get('parameters', [])
- code_params = code_info.get('parameters', [])
-
- # Compare parameter counts
- if len(docs_params) != len(code_params):
- return {
- 'severity': 'medium',
- 'difference': f"Parameter count mismatch: docs has {len(docs_params)}, code has {len(code_params)}",
- 'suggestion': f"Documentation shows {len(docs_params)} parameters, but code has {len(code_params)}"
- }
-
- # Compare parameter names and types
- for i, (doc_param, code_param) in enumerate(zip(docs_params, code_params)):
- doc_name = doc_param.get('name', '')
- code_name = code_param.get('name', '')
-
- # Parameter name mismatch
- if doc_name != code_name:
- # Use fuzzy matching for slight variations
- similarity = SequenceMatcher(None, doc_name, code_name).ratio()
- if similarity < 0.8: # Not similar enough
- return {
- 'severity': 'medium',
- 'difference': f"Parameter {i+1} name mismatch: '{doc_name}' in docs vs '{code_name}' in code",
- 'suggestion': f"Update documentation to use parameter name '{code_name}'"
- }
-
- # Type mismatch
- doc_type = doc_param.get('type')
- code_type = code_param.get('type_hint')
-
- if doc_type and code_type and doc_type != code_type:
- return {
- 'severity': 'low',
- 'difference': f"Parameter '{doc_name}' type mismatch: '{doc_type}' in docs vs '{code_type}' in code",
- 'suggestion': f"Verify correct type for parameter '{doc_name}'"
- }
-
- # Compare return types if both have them
- docs_return = docs_info.get('return_type')
- code_return = code_info.get('return_type')
-
- if docs_return and code_return and docs_return != code_return:
- return {
- 'severity': 'low',
- 'difference': f"Return type mismatch: '{docs_return}' in docs vs '{code_return}' in code",
- 'suggestion': "Verify correct return type"
- }
-
- return None
-
- def generate_summary(self, conflicts: List[Conflict]) -> Dict[str, Any]:
- """
- Generate summary statistics for conflicts.
-
- Args:
- conflicts: List of Conflict objects
-
- Returns:
- Summary dict with statistics
- """
- summary = {
- 'total': len(conflicts),
- 'by_type': {},
- 'by_severity': {},
- 'apis_affected': len(set(c.api_name for c in conflicts))
- }
-
- # Count by type
- for conflict_type in ['missing_in_docs', 'missing_in_code', 'signature_mismatch', 'description_mismatch']:
- count = sum(1 for c in conflicts if c.type == conflict_type)
- summary['by_type'][conflict_type] = count
-
- # Count by severity
- for severity in ['low', 'medium', 'high']:
- count = sum(1 for c in conflicts if c.severity == severity)
- summary['by_severity'][severity] = count
-
- return summary
-
- def save_conflicts(self, conflicts: List[Conflict], output_path: str):
- """
- Save conflicts to JSON file.
-
- Args:
- conflicts: List of Conflict objects
- output_path: Path to output JSON file
- """
- data = {
- 'conflicts': [asdict(c) for c in conflicts],
- 'summary': self.generate_summary(conflicts)
- }
-
- with open(output_path, 'w', encoding='utf-8') as f:
- json.dump(data, f, indent=2, ensure_ascii=False)
-
- logger.info(f"Conflicts saved to: {output_path}")
-
-
-if __name__ == '__main__':
- import sys
-
- if len(sys.argv) < 3:
- print("Usage: python conflict_detector.py ")
- sys.exit(1)
-
- docs_file = sys.argv[1]
- github_file = sys.argv[2]
-
- # Load data
- with open(docs_file, 'r') as f:
- docs_data = json.load(f)
-
- with open(github_file, 'r') as f:
- github_data = json.load(f)
-
- # Detect conflicts
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector.detect_all_conflicts()
-
- # Print summary
- summary = detector.generate_summary(conflicts)
- print("\n📊 Conflict Summary:")
- print(f" Total conflicts: {summary['total']}")
- print(f" APIs affected: {summary['apis_affected']}")
- print("\n By Type:")
- for conflict_type, count in summary['by_type'].items():
- if count > 0:
- print(f" {conflict_type}: {count}")
- print("\n By Severity:")
- for severity, count in summary['by_severity'].items():
- if count > 0:
- emoji = '🔴' if severity == 'high' else '🟡' if severity == 'medium' else '🟢'
- print(f" {emoji} {severity}: {count}")
-
- # Save to file
- output_file = 'conflicts.json'
- detector.save_conflicts(conflicts, output_file)
- print(f"\n✅ Full report saved to: {output_file}")
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/constants.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/constants.py
deleted file mode 100644
index 2685e93..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/constants.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""Configuration constants for Skill Seekers CLI.
-
-This module centralizes all magic numbers and configuration values used
-across the CLI tools to improve maintainability and clarity.
-"""
-
-# ===== SCRAPING CONFIGURATION =====
-
-# Default scraping limits
-DEFAULT_RATE_LIMIT = 0.5 # seconds between requests
-DEFAULT_MAX_PAGES = 500 # maximum pages to scrape
-DEFAULT_CHECKPOINT_INTERVAL = 1000 # pages between checkpoints
-DEFAULT_ASYNC_MODE = False # use async mode for parallel scraping (opt-in)
-
-# Content analysis limits
-CONTENT_PREVIEW_LENGTH = 500 # characters to check for categorization
-MAX_PAGES_WARNING_THRESHOLD = 10000 # warn if config exceeds this
-
-# Quality thresholds
-MIN_CATEGORIZATION_SCORE = 2 # minimum score for category assignment
-URL_MATCH_POINTS = 3 # points for URL keyword match
-TITLE_MATCH_POINTS = 2 # points for title keyword match
-CONTENT_MATCH_POINTS = 1 # points for content keyword match
-
-# ===== ENHANCEMENT CONFIGURATION =====
-
-# API-based enhancement limits (uses Anthropic API)
-API_CONTENT_LIMIT = 100000 # max characters for API enhancement
-API_PREVIEW_LIMIT = 40000 # max characters for preview
-
-# Local enhancement limits (uses Claude Code Max)
-LOCAL_CONTENT_LIMIT = 50000 # max characters for local enhancement
-LOCAL_PREVIEW_LIMIT = 20000 # max characters for preview
-
-# ===== PAGE ESTIMATION =====
-
-# Estimation and discovery settings
-DEFAULT_MAX_DISCOVERY = 1000 # default max pages to discover
-DISCOVERY_THRESHOLD = 10000 # threshold for warnings
-
-# ===== FILE LIMITS =====
-
-# Output and processing limits
-MAX_REFERENCE_FILES = 100 # maximum reference files per skill
-MAX_CODE_BLOCKS_PER_PAGE = 5 # maximum code blocks to extract per page
-
-# ===== EXPORT CONSTANTS =====
-
-__all__ = [
- # Scraping
- 'DEFAULT_RATE_LIMIT',
- 'DEFAULT_MAX_PAGES',
- 'DEFAULT_CHECKPOINT_INTERVAL',
- 'DEFAULT_ASYNC_MODE',
- 'CONTENT_PREVIEW_LENGTH',
- 'MAX_PAGES_WARNING_THRESHOLD',
- 'MIN_CATEGORIZATION_SCORE',
- 'URL_MATCH_POINTS',
- 'TITLE_MATCH_POINTS',
- 'CONTENT_MATCH_POINTS',
- # Enhancement
- 'API_CONTENT_LIMIT',
- 'API_PREVIEW_LIMIT',
- 'LOCAL_CONTENT_LIMIT',
- 'LOCAL_PREVIEW_LIMIT',
- # Estimation
- 'DEFAULT_MAX_DISCOVERY',
- 'DISCOVERY_THRESHOLD',
- # Limits
- 'MAX_REFERENCE_FILES',
- 'MAX_CODE_BLOCKS_PER_PAGE',
-]
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/doc_scraper.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/doc_scraper.py
deleted file mode 100644
index 963780d..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/doc_scraper.py
+++ /dev/null
@@ -1,1822 +0,0 @@
-#!/usr/bin/env python3
-"""
-Documentation to Claude Skill Converter
-Single tool to scrape any documentation and create high-quality Claude skills.
-
-Usage:
- skill-seekers scrape --interactive
- skill-seekers scrape --config configs/godot.json
- skill-seekers scrape --url https://react.dev/ --name react
-"""
-
-import os
-import sys
-import json
-import time
-import re
-import argparse
-import hashlib
-import logging
-import asyncio
-import requests
-import httpx
-from pathlib import Path
-from urllib.parse import urljoin, urlparse
-from bs4 import BeautifulSoup
-from collections import deque, defaultdict
-from typing import Optional, Dict, List, Tuple, Set, Deque, Any
-
-# Add parent directory to path for imports when run as script
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.llms_txt_detector import LlmsTxtDetector
-from skill_seekers.cli.llms_txt_parser import LlmsTxtParser
-from skill_seekers.cli.llms_txt_downloader import LlmsTxtDownloader
-from skill_seekers.cli.constants import (
- DEFAULT_RATE_LIMIT,
- DEFAULT_MAX_PAGES,
- DEFAULT_CHECKPOINT_INTERVAL,
- DEFAULT_ASYNC_MODE,
- CONTENT_PREVIEW_LENGTH,
- MAX_PAGES_WARNING_THRESHOLD,
- MIN_CATEGORIZATION_SCORE
-)
-
-# Configure logging
-logger = logging.getLogger(__name__)
-
-
-def setup_logging(verbose: bool = False, quiet: bool = False) -> None:
- """Configure logging based on verbosity level.
-
- Args:
- verbose: Enable DEBUG level logging
- quiet: Enable WARNING level logging only
- """
- if quiet:
- level = logging.WARNING
- elif verbose:
- level = logging.DEBUG
- else:
- level = logging.INFO
-
- logging.basicConfig(
- level=level,
- format='%(message)s',
- force=True
- )
-
-
-class DocToSkillConverter:
- def __init__(self, config: Dict[str, Any], dry_run: bool = False, resume: bool = False) -> None:
- self.config = config
- self.name = config['name']
- self.base_url = config['base_url']
- self.dry_run = dry_run
- self.resume = resume
-
- # Paths
- self.data_dir = f"output/{self.name}_data"
- self.skill_dir = f"output/{self.name}"
- self.checkpoint_file = f"{self.data_dir}/checkpoint.json"
-
- # Checkpoint config
- checkpoint_config = config.get('checkpoint', {})
- self.checkpoint_enabled = checkpoint_config.get('enabled', False)
- self.checkpoint_interval = checkpoint_config.get('interval', DEFAULT_CHECKPOINT_INTERVAL)
-
- # llms.txt detection state
- skip_llms_txt_value = config.get('skip_llms_txt', False)
- if not isinstance(skip_llms_txt_value, bool):
- logger.warning(
- "Invalid value for 'skip_llms_txt': %r (expected bool). Defaulting to False.",
- skip_llms_txt_value
- )
- self.skip_llms_txt = False
- else:
- self.skip_llms_txt = skip_llms_txt_value
- self.llms_txt_detected = False
- self.llms_txt_variant = None
- self.llms_txt_variants: List[str] = [] # Track all downloaded variants
-
- # Parallel scraping config
- self.workers = config.get('workers', 1)
- self.async_mode = config.get('async_mode', DEFAULT_ASYNC_MODE)
-
- # State
- self.visited_urls: set[str] = set()
- # Support multiple starting URLs
- start_urls = config.get('start_urls', [self.base_url])
- self.pending_urls = deque(start_urls)
- self.pages: List[Dict[str, Any]] = []
- self.pages_scraped = 0
-
- # Thread-safe lock for parallel scraping
- if self.workers > 1:
- import threading
- self.lock = threading.Lock()
-
- # Create directories (unless dry-run)
- if not dry_run:
- os.makedirs(f"{self.data_dir}/pages", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/references", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/scripts", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/assets", exist_ok=True)
-
- # Load checkpoint if resuming
- if resume and not dry_run:
- self.load_checkpoint()
-
- def is_valid_url(self, url: str) -> bool:
- """Check if URL should be scraped based on patterns.
-
- Args:
- url (str): URL to validate
-
- Returns:
- bool: True if URL matches include patterns and doesn't match exclude patterns
- """
- if not url.startswith(self.base_url):
- return False
-
- # Include patterns
- includes = self.config.get('url_patterns', {}).get('include', [])
- if includes and not any(pattern in url for pattern in includes):
- return False
-
- # Exclude patterns
- excludes = self.config.get('url_patterns', {}).get('exclude', [])
- if any(pattern in url for pattern in excludes):
- return False
-
- return True
-
- def save_checkpoint(self) -> None:
- """Save progress checkpoint"""
- if not self.checkpoint_enabled or self.dry_run:
- return
-
- checkpoint_data = {
- "config": self.config,
- "visited_urls": list(self.visited_urls),
- "pending_urls": list(self.pending_urls),
- "pages_scraped": self.pages_scraped,
- "last_updated": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
- "checkpoint_interval": self.checkpoint_interval
- }
-
- try:
- with open(self.checkpoint_file, 'w') as f:
- json.dump(checkpoint_data, f, indent=2)
- logger.info(" 💾 Checkpoint saved (%d pages)", self.pages_scraped)
- except Exception as e:
- logger.warning(" ⚠️ Failed to save checkpoint: %s", e)
-
- def load_checkpoint(self) -> None:
- """Load progress from checkpoint"""
- if not os.path.exists(self.checkpoint_file):
- logger.info("ℹ️ No checkpoint found, starting fresh")
- return
-
- try:
- with open(self.checkpoint_file, 'r') as f:
- checkpoint_data = json.load(f)
-
- self.visited_urls = set(checkpoint_data["visited_urls"])
- self.pending_urls = deque(checkpoint_data["pending_urls"])
- self.pages_scraped = checkpoint_data["pages_scraped"]
-
- logger.info("✅ Resumed from checkpoint")
- logger.info(" Pages already scraped: %d", self.pages_scraped)
- logger.info(" URLs visited: %d", len(self.visited_urls))
- logger.info(" URLs pending: %d", len(self.pending_urls))
- logger.info(" Last updated: %s", checkpoint_data['last_updated'])
- logger.info("")
-
- except Exception as e:
- logger.warning("⚠️ Failed to load checkpoint: %s", e)
- logger.info(" Starting fresh")
-
- def clear_checkpoint(self) -> None:
- """Remove checkpoint file"""
- if os.path.exists(self.checkpoint_file):
- try:
- os.remove(self.checkpoint_file)
- logger.info("✅ Checkpoint cleared")
- except Exception as e:
- logger.warning("⚠️ Failed to clear checkpoint: %s", e)
-
- def extract_content(self, soup: Any, url: str) -> Dict[str, Any]:
- """Extract content with improved code and pattern detection"""
- page = {
- 'url': url,
- 'title': '',
- 'content': '',
- 'headings': [],
- 'code_samples': [],
- 'patterns': [], # NEW: Extract common patterns
- 'links': []
- }
-
- selectors = self.config.get('selectors', {})
-
- # Extract title
- title_elem = soup.select_one(selectors.get('title', 'title'))
- if title_elem:
- page['title'] = self.clean_text(title_elem.get_text())
-
- # Find main content
- main_selector = selectors.get('main_content', 'div[role="main"]')
- main = soup.select_one(main_selector)
-
- if not main:
- logger.warning("⚠ No content: %s", url)
- return page
-
- # Extract headings with better structure
- for h in main.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']):
- text = self.clean_text(h.get_text())
- if text:
- page['headings'].append({
- 'level': h.name,
- 'text': text,
- 'id': h.get('id', '')
- })
-
- # Extract code with language detection
- code_selector = selectors.get('code_blocks', 'pre code')
- for code_elem in main.select(code_selector):
- code = code_elem.get_text()
- if len(code.strip()) > 10:
- # Try to detect language
- lang = self.detect_language(code_elem, code)
- page['code_samples'].append({
- 'code': code.strip(),
- 'language': lang
- })
-
- # Extract patterns (NEW: common code patterns)
- page['patterns'] = self.extract_patterns(main, page['code_samples'])
-
- # Extract paragraphs
- paragraphs = []
- for p in main.find_all('p'):
- text = self.clean_text(p.get_text())
- if text and len(text) > 20: # Skip very short paragraphs
- paragraphs.append(text)
-
- page['content'] = '\n\n'.join(paragraphs)
-
- # Extract links from entire page (not just main content)
- # This allows discovery of navigation links outside the main content area
- for link in soup.find_all('a', href=True):
- href = urljoin(url, link['href'])
- # Strip anchor fragments to avoid treating #anchors as separate pages
- href = href.split('#')[0]
- if self.is_valid_url(href) and href not in page['links']:
- page['links'].append(href)
-
- return page
-
- def _extract_language_from_classes(self, classes):
- """Extract language from class list
-
- Supports multiple patterns:
- - language-{lang} (e.g., "language-python")
- - lang-{lang} (e.g., "lang-javascript")
- - brush: {lang} (e.g., "brush: java")
- - bare language name (e.g., "python", "java")
-
- """
- # Define common programming languages
- known_languages = [
- "javascript", "java", "xml", "html", "python", "bash", "cpp", "typescript",
- "go", "rust", "php", "ruby", "swift", "kotlin", "csharp", "c", "sql",
- "yaml", "json", "markdown", "css", "scss", "sass", "jsx", "tsx", "vue",
- "shell", "powershell", "r", "scala", "dart", "perl", "lua", "elixir"
- ]
-
- for cls in classes:
- # Clean special characters (except word chars and hyphens)
- cls = re.sub(r'[^\w-]', '', cls)
-
- if 'language-' in cls:
- return cls.replace('language-', '')
-
- if 'lang-' in cls:
- return cls.replace('lang-', '')
-
- # Check for brush: pattern (e.g., "brush: java")
- if 'brush' in cls.lower():
- lang = cls.lower().replace('brush', '').strip()
- if lang in known_languages:
- return lang
-
- # Check for bare language name
- if cls in known_languages:
- return cls
-
- return None
-
- def detect_language(self, elem, code):
- """Detect programming language from code block"""
-
- # Check element classes
- lang = self._extract_language_from_classes(elem.get('class', []))
- if lang:
- return lang
-
- # Check parent pre element
- parent = elem.parent
- if parent and parent.name == 'pre':
- lang = self._extract_language_from_classes(parent.get('class', []))
- if lang:
- return lang
-
- # Heuristic detection
- if 'import ' in code and 'from ' in code:
- return 'python'
- if 'const ' in code or 'let ' in code or '=>' in code:
- return 'javascript'
- if 'func ' in code and 'var ' in code:
- return 'gdscript'
- if 'def ' in code and ':' in code:
- return 'python'
- if '#include' in code or 'int main' in code:
- return 'cpp'
- # C# detection
- if 'using System' in code or 'namespace ' in code:
- return 'csharp'
- if '{ get; set; }' in code:
- return 'csharp'
- if any(keyword in code for keyword in ['public class ', 'private class ', 'internal class ', 'public static void ']):
- return 'csharp'
-
- return 'unknown'
-
- def extract_patterns(self, main: Any, code_samples: List[Dict[str, Any]]) -> List[Dict[str, str]]:
- """Extract common coding patterns (NEW FEATURE)"""
- patterns = []
-
- # Look for "Example:" or "Pattern:" sections
- for elem in main.find_all(['p', 'div']):
- text = elem.get_text().lower()
- if any(word in text for word in ['example:', 'pattern:', 'usage:', 'typical use']):
- # Get the code that follows
- next_code = elem.find_next(['pre', 'code'])
- if next_code:
- patterns.append({
- 'description': self.clean_text(elem.get_text()),
- 'code': next_code.get_text().strip()
- })
-
- return patterns[:5] # Limit to 5 most relevant patterns
-
- def clean_text(self, text: str) -> str:
- """Clean text content"""
- text = re.sub(r'\s+', ' ', text)
- return text.strip()
-
- def save_page(self, page: Dict[str, Any]) -> None:
- """Save page data"""
- url_hash = hashlib.md5(page['url'].encode()).hexdigest()[:10]
- safe_title = re.sub(r'[^\w\s-]', '', page['title'])[:50]
- safe_title = re.sub(r'[-\s]+', '_', safe_title)
-
- filename = f"{safe_title}_{url_hash}.json"
- filepath = os.path.join(self.data_dir, "pages", filename)
-
- with open(filepath, 'w', encoding='utf-8') as f:
- json.dump(page, f, indent=2, ensure_ascii=False)
-
- def scrape_page(self, url: str) -> None:
- """Scrape a single page with thread-safe operations.
-
- Args:
- url (str): URL to scrape
-
- Returns:
- dict or None: Page data dict on success, None on failure
-
- Note:
- Uses threading locks when workers > 1 for thread safety
- """
- try:
- # Scraping part (no lock needed - independent)
- headers = {'User-Agent': 'Mozilla/5.0 (Documentation Scraper)'}
- response = requests.get(url, headers=headers, timeout=30)
- response.raise_for_status()
-
- soup = BeautifulSoup(response.content, 'html.parser')
- page = self.extract_content(soup, url)
-
- # Thread-safe operations (lock required)
- if self.workers > 1:
- with self.lock:
- logger.info(" %s", url)
- self.save_page(page)
- self.pages.append(page)
-
- # Add new URLs
- for link in page['links']:
- if link not in self.visited_urls and link not in self.pending_urls:
- self.pending_urls.append(link)
- else:
- # Single-threaded mode (no lock needed)
- logger.info(" %s", url)
- self.save_page(page)
- self.pages.append(page)
-
- # Add new URLs
- for link in page['links']:
- if link not in self.visited_urls and link not in self.pending_urls:
- self.pending_urls.append(link)
-
- # Rate limiting
- rate_limit = self.config.get('rate_limit', DEFAULT_RATE_LIMIT)
- if rate_limit > 0:
- time.sleep(rate_limit)
-
- except Exception as e:
- if self.workers > 1:
- with self.lock:
- logger.error(" ✗ Error scraping %s: %s: %s", url, type(e).__name__, e)
- else:
- logger.error(" ✗ Error scraping page: %s: %s", type(e).__name__, e)
- logger.error(" URL: %s", url)
-
- async def scrape_page_async(self, url: str, semaphore: asyncio.Semaphore, client: httpx.AsyncClient) -> None:
- """Scrape a single page asynchronously.
-
- Args:
- url: URL to scrape
- semaphore: Asyncio semaphore for concurrency control
- client: Shared httpx AsyncClient for connection pooling
-
- Note:
- Uses asyncio.Lock for async-safe operations instead of threading.Lock
- """
- async with semaphore: # Limit concurrent requests
- try:
- # Async HTTP request
- headers = {'User-Agent': 'Mozilla/5.0 (Documentation Scraper)'}
- response = await client.get(url, headers=headers, timeout=30.0)
- response.raise_for_status()
-
- # BeautifulSoup parsing (still synchronous, but fast)
- soup = BeautifulSoup(response.content, 'html.parser')
- page = self.extract_content(soup, url)
-
- # Async-safe operations (no lock needed - single event loop)
- logger.info(" %s", url)
- self.save_page(page)
- self.pages.append(page)
-
- # Add new URLs
- for link in page['links']:
- if link not in self.visited_urls and link not in self.pending_urls:
- self.pending_urls.append(link)
-
- # Rate limiting
- rate_limit = self.config.get('rate_limit', DEFAULT_RATE_LIMIT)
- if rate_limit > 0:
- await asyncio.sleep(rate_limit)
-
- except Exception as e:
- logger.error(" ✗ Error scraping %s: %s: %s", url, type(e).__name__, e)
-
- def _try_llms_txt(self) -> bool:
- """
- Try to use llms.txt instead of HTML scraping.
- Downloads ALL available variants and stores with .md extension.
-
- Returns:
- True if llms.txt was found and processed successfully
- """
- logger.info("\n🔍 Checking for llms.txt at %s...", self.base_url)
-
- # Check for explicit config URL first
- explicit_url = self.config.get('llms_txt_url')
- if explicit_url:
- logger.info("\n📌 Using explicit llms_txt_url from config: %s", explicit_url)
-
- # Download explicit file first
- downloader = LlmsTxtDownloader(explicit_url)
- content = downloader.download()
-
- if content:
- # Save explicit file with proper .md extension
- filename = downloader.get_proper_filename()
- filepath = os.path.join(self.skill_dir, "references", filename)
- os.makedirs(os.path.dirname(filepath), exist_ok=True)
-
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(content)
- logger.info(" 💾 Saved %s (%d chars)", filename, len(content))
-
- # Also try to detect and download ALL other variants
- detector = LlmsTxtDetector(self.base_url)
- variants = detector.detect_all()
-
- if variants:
- logger.info("\n🔍 Found %d total variant(s), downloading remaining...", len(variants))
- for variant_info in variants:
- url = variant_info['url']
- variant = variant_info['variant']
-
- # Skip the explicit one we already downloaded
- if url == explicit_url:
- continue
-
- logger.info(" 📥 Downloading %s...", variant)
- extra_downloader = LlmsTxtDownloader(url)
- extra_content = extra_downloader.download()
-
- if extra_content:
- extra_filename = extra_downloader.get_proper_filename()
- extra_filepath = os.path.join(self.skill_dir, "references", extra_filename)
- with open(extra_filepath, 'w', encoding='utf-8') as f:
- f.write(extra_content)
- logger.info(" ✓ %s (%d chars)", extra_filename, len(extra_content))
-
- # Parse explicit file for skill building
- parser = LlmsTxtParser(content)
- pages = parser.parse()
-
- if pages:
- for page in pages:
- self.save_page(page)
- self.pages.append(page)
-
- self.llms_txt_detected = True
- self.llms_txt_variant = 'explicit'
- return True
-
- # Auto-detection: Find ALL variants
- detector = LlmsTxtDetector(self.base_url)
- variants = detector.detect_all()
-
- if not variants:
- logger.info("ℹ️ No llms.txt found, using HTML scraping")
- return False
-
- logger.info("✅ Found %d llms.txt variant(s)", len(variants))
-
- # Download ALL variants
- downloaded = {}
- for variant_info in variants:
- url = variant_info['url']
- variant = variant_info['variant']
-
- logger.info(" 📥 Downloading %s...", variant)
- downloader = LlmsTxtDownloader(url)
- content = downloader.download()
-
- if content:
- filename = downloader.get_proper_filename()
- downloaded[variant] = {
- 'content': content,
- 'filename': filename,
- 'size': len(content)
- }
- logger.info(" ✓ %s (%d chars)", filename, len(content))
-
- if not downloaded:
- logger.warning("⚠️ Failed to download any variants, falling back to HTML scraping")
- return False
-
- # Save ALL variants to references/
- os.makedirs(os.path.join(self.skill_dir, "references"), exist_ok=True)
-
- for variant, data in downloaded.items():
- filepath = os.path.join(self.skill_dir, "references", data['filename'])
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(data['content'])
- logger.info(" 💾 Saved %s", data['filename'])
-
- # Parse LARGEST variant for skill building
- largest = max(downloaded.items(), key=lambda x: x[1]['size'])
- logger.info("\n📄 Parsing %s for skill building...", largest[1]['filename'])
-
- parser = LlmsTxtParser(largest[1]['content'])
- pages = parser.parse()
-
- if not pages:
- logger.warning("⚠️ Failed to parse llms.txt, falling back to HTML scraping")
- return False
-
- logger.info(" ✓ Parsed %d sections", len(pages))
-
- # Save pages for skill building
- for page in pages:
- self.save_page(page)
- self.pages.append(page)
-
- self.llms_txt_detected = True
- self.llms_txt_variants = list(downloaded.keys())
-
- return True
-
- def scrape_all(self) -> None:
- """Scrape all pages (supports llms.txt and HTML scraping)
-
- Routes to async version if async_mode is enabled in config.
- """
- # Route to async version if enabled
- if self.async_mode:
- asyncio.run(self.scrape_all_async())
- return
-
- # Try llms.txt first (unless dry-run or explicitly disabled)
- if not self.dry_run and not self.skip_llms_txt:
- llms_result = self._try_llms_txt()
- if llms_result:
- logger.info("\n✅ Used llms.txt (%s) - skipping HTML scraping", self.llms_txt_variant)
- self.save_summary()
- return
-
- # HTML scraping (sync/thread-based logic)
- logger.info("\n" + "=" * 60)
- if self.dry_run:
- logger.info("DRY RUN: %s", self.name)
- else:
- logger.info("SCRAPING: %s", self.name)
- logger.info("=" * 60)
- logger.info("Base URL: %s", self.base_url)
-
- if self.dry_run:
- logger.info("Mode: Preview only (no actual scraping)\n")
- else:
- logger.info("Output: %s", self.data_dir)
- if self.workers > 1:
- logger.info("Workers: %d parallel threads", self.workers)
- logger.info("")
-
- max_pages = self.config.get('max_pages', DEFAULT_MAX_PAGES)
-
- # Handle unlimited mode
- if max_pages is None or max_pages == -1:
- logger.warning("⚠️ UNLIMITED MODE: No page limit (will scrape all pages)\n")
- unlimited = True
- else:
- unlimited = False
-
- # Dry run: preview first 20 URLs
- preview_limit = 20 if self.dry_run else max_pages
-
- # Single-threaded mode (original sequential logic)
- if self.workers <= 1:
- while self.pending_urls and (unlimited or len(self.visited_urls) < preview_limit):
- url = self.pending_urls.popleft()
-
- if url in self.visited_urls:
- continue
-
- self.visited_urls.add(url)
-
- if self.dry_run:
- # Just show what would be scraped
- logger.info(" [Preview] %s", url)
- try:
- headers = {'User-Agent': 'Mozilla/5.0 (Documentation Scraper - Dry Run)'}
- response = requests.get(url, headers=headers, timeout=10)
- soup = BeautifulSoup(response.content, 'html.parser')
-
- main_selector = self.config.get('selectors', {}).get('main_content', 'div[role="main"]')
- main = soup.select_one(main_selector)
-
- if main:
- for link in main.find_all('a', href=True):
- href = urljoin(url, link['href'])
- if self.is_valid_url(href) and href not in self.visited_urls:
- self.pending_urls.append(href)
- except Exception as e:
- # Failed to extract links in fast mode, continue anyway
- logger.warning("⚠️ Warning: Could not extract links from %s: %s", url, e)
- else:
- self.scrape_page(url)
- self.pages_scraped += 1
-
- if self.checkpoint_enabled and self.pages_scraped % self.checkpoint_interval == 0:
- self.save_checkpoint()
-
- if len(self.visited_urls) % 10 == 0:
- logger.info(" [%d pages]", len(self.visited_urls))
-
- # Multi-threaded mode (parallel scraping)
- else:
- from concurrent.futures import ThreadPoolExecutor, as_completed
-
- logger.info("🚀 Starting parallel scraping with %d workers\n", self.workers)
-
- with ThreadPoolExecutor(max_workers=self.workers) as executor:
- futures = []
-
- while self.pending_urls and (unlimited or len(self.visited_urls) < preview_limit):
- # Get next batch of URLs (thread-safe)
- batch = []
- batch_size = min(self.workers * 2, len(self.pending_urls))
-
- with self.lock:
- for _ in range(batch_size):
- if not self.pending_urls:
- break
- url = self.pending_urls.popleft()
-
- if url not in self.visited_urls:
- self.visited_urls.add(url)
- batch.append(url)
-
- # Submit batch to executor
- for url in batch:
- if unlimited or len(self.visited_urls) <= preview_limit:
- future = executor.submit(self.scrape_page, url)
- futures.append(future)
-
- # Wait for some to complete before submitting more
- completed = 0
- for future in as_completed(futures[:batch_size]):
- # Check for exceptions
- try:
- future.result() # Raises exception if scrape_page failed
- except Exception as e:
- with self.lock:
- logger.warning(" ⚠️ Worker exception: %s", e)
-
- completed += 1
-
- with self.lock:
- self.pages_scraped += 1
-
- if self.checkpoint_enabled and self.pages_scraped % self.checkpoint_interval == 0:
- self.save_checkpoint()
-
- if self.pages_scraped % 10 == 0:
- logger.info(" [%d pages scraped]", self.pages_scraped)
-
- # Remove completed futures
- futures = [f for f in futures if not f.done()]
-
- # Wait for remaining futures
- for future in as_completed(futures):
- # Check for exceptions
- try:
- future.result()
- except Exception as e:
- with self.lock:
- logger.warning(" ⚠️ Worker exception: %s", e)
-
- with self.lock:
- self.pages_scraped += 1
-
- if self.dry_run:
- logger.info("\n✅ Dry run complete: would scrape ~%d pages", len(self.visited_urls))
- if len(self.visited_urls) >= preview_limit:
- logger.info(" (showing first %d, actual scraping may find more)", preview_limit)
- logger.info("\n💡 To actually scrape, run without --dry-run")
- else:
- logger.info("\n✅ Scraped %d pages", len(self.visited_urls))
- self.save_summary()
-
- async def scrape_all_async(self) -> None:
- """Scrape all pages asynchronously (async/await version).
-
- This method provides significantly better performance for parallel scraping
- compared to thread-based scraping, with lower memory overhead and better
- CPU utilization.
-
- Performance: ~2-3x faster than sync mode with same worker count.
- """
- # Try llms.txt first (unless dry-run or explicitly disabled)
- if not self.dry_run and not self.skip_llms_txt:
- llms_result = self._try_llms_txt()
- if llms_result:
- logger.info("\n✅ Used llms.txt (%s) - skipping HTML scraping", self.llms_txt_variant)
- self.save_summary()
- return
-
- # HTML scraping (async version)
- logger.info("\n" + "=" * 60)
- if self.dry_run:
- logger.info("DRY RUN (ASYNC): %s", self.name)
- else:
- logger.info("SCRAPING (ASYNC): %s", self.name)
- logger.info("=" * 60)
- logger.info("Base URL: %s", self.base_url)
-
- if self.dry_run:
- logger.info("Mode: Preview only (no actual scraping)\n")
- else:
- logger.info("Output: %s", self.data_dir)
- logger.info("Workers: %d concurrent tasks (async)", self.workers)
- logger.info("")
-
- max_pages = self.config.get('max_pages', DEFAULT_MAX_PAGES)
-
- # Handle unlimited mode
- if max_pages is None or max_pages == -1:
- logger.warning("⚠️ UNLIMITED MODE: No page limit (will scrape all pages)\n")
- unlimited = True
- preview_limit = float('inf')
- else:
- unlimited = False
- preview_limit = 20 if self.dry_run else max_pages
-
- # Create semaphore for concurrency control
- semaphore = asyncio.Semaphore(self.workers)
-
- # Create shared HTTP client with connection pooling
- async with httpx.AsyncClient(
- timeout=30.0,
- limits=httpx.Limits(max_connections=self.workers * 2)
- ) as client:
- tasks = []
-
- while self.pending_urls and (unlimited or len(self.visited_urls) < preview_limit):
- # Get next batch of URLs
- batch = []
- batch_size = min(self.workers * 2, len(self.pending_urls))
-
- for _ in range(batch_size):
- if not self.pending_urls:
- break
- url = self.pending_urls.popleft()
-
- if url not in self.visited_urls:
- self.visited_urls.add(url)
- batch.append(url)
-
- # Create async tasks for batch
- for url in batch:
- if unlimited or len(self.visited_urls) <= preview_limit:
- if self.dry_run:
- logger.info(" [Preview] %s", url)
- else:
- task = asyncio.create_task(
- self.scrape_page_async(url, semaphore, client)
- )
- tasks.append(task)
-
- # Wait for batch to complete before continuing
- if tasks:
- await asyncio.gather(*tasks, return_exceptions=True)
- tasks = []
- self.pages_scraped = len(self.visited_urls)
-
- # Progress indicator
- if self.pages_scraped % 10 == 0 and not self.dry_run:
- logger.info(" [%d pages scraped]", self.pages_scraped)
-
- # Checkpoint saving
- if not self.dry_run and self.checkpoint_enabled:
- if self.pages_scraped % self.checkpoint_interval == 0:
- self.save_checkpoint()
-
- # Wait for any remaining tasks
- if tasks:
- await asyncio.gather(*tasks, return_exceptions=True)
-
- if self.dry_run:
- logger.info("\n✅ Dry run complete: would scrape ~%d pages", len(self.visited_urls))
- if len(self.visited_urls) >= preview_limit:
- logger.info(" (showing first %d, actual scraping may find more)", int(preview_limit))
- logger.info("\n💡 To actually scrape, run without --dry-run")
- else:
- logger.info("\n✅ Scraped %d pages (async mode)", len(self.visited_urls))
- self.save_summary()
-
- def save_summary(self) -> None:
- """Save scraping summary"""
- summary = {
- 'name': self.name,
- 'total_pages': len(self.pages),
- 'base_url': self.base_url,
- 'llms_txt_detected': self.llms_txt_detected,
- 'llms_txt_variant': self.llms_txt_variant,
- 'pages': [{'title': p['title'], 'url': p['url']} for p in self.pages]
- }
-
- with open(f"{self.data_dir}/summary.json", 'w', encoding='utf-8') as f:
- json.dump(summary, f, indent=2, ensure_ascii=False)
-
- def load_scraped_data(self) -> List[Dict[str, Any]]:
- """Load previously scraped data"""
- pages = []
- pages_dir = Path(self.data_dir) / "pages"
-
- if not pages_dir.exists():
- return []
-
- for json_file in pages_dir.glob("*.json"):
- try:
- with open(json_file, 'r', encoding='utf-8') as f:
- pages.append(json.load(f))
- except Exception as e:
- logger.error("⚠️ Error loading scraped data file %s: %s: %s", json_file, type(e).__name__, e)
- logger.error(" Suggestion: File may be corrupted, consider re-scraping with --fresh")
-
- return pages
-
- def smart_categorize(self, pages: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
- """Improved categorization with better pattern matching"""
- category_defs = self.config.get('categories', {})
-
- # Default smart categories if none provided
- if not category_defs:
- category_defs = self.infer_categories(pages)
-
- categories: Dict[str, List[Dict[str, Any]]] = {cat: [] for cat in category_defs.keys()}
- categories['other'] = []
-
- for page in pages:
- url = page['url'].lower()
- title = page['title'].lower()
- content = page.get('content', '').lower()[:CONTENT_PREVIEW_LENGTH] # Check first N chars for categorization
-
- categorized = False
-
- # Match against keywords
- for cat, keywords in category_defs.items():
- score = 0
- for keyword in keywords:
- keyword = keyword.lower()
- if keyword in url:
- score += 3
- if keyword in title:
- score += 2
- if keyword in content:
- score += 1
-
- if score >= MIN_CATEGORIZATION_SCORE: # Threshold for categorization
- categories[cat].append(page)
- categorized = True
- break
-
- if not categorized:
- categories['other'].append(page)
-
- # Remove empty categories
- categories = {k: v for k, v in categories.items() if v}
-
- return categories
-
- def infer_categories(self, pages: List[Dict[str, Any]]) -> Dict[str, List[str]]:
- """Infer categories from URL patterns (IMPROVED)"""
- url_segments: defaultdict[str, int] = defaultdict(int)
-
- for page in pages:
- path = urlparse(page['url']).path
- segments = [s for s in path.split('/') if s and s not in ['en', 'stable', 'latest', 'docs']]
-
- for seg in segments:
- url_segments[seg] += 1
-
- # Top segments become categories
- top_segments = sorted(url_segments.items(), key=lambda x: x[1], reverse=True)[:8]
-
- categories = {}
- for seg, count in top_segments:
- if count >= 3: # At least 3 pages
- categories[seg] = [seg]
-
- # Add common defaults
- if 'tutorial' not in categories and any('tutorial' in url for url in [p['url'] for p in pages]):
- categories['tutorials'] = ['tutorial', 'guide', 'getting-started']
-
- if 'api' not in categories and any('api' in url or 'reference' in url for url in [p['url'] for p in pages]):
- categories['api'] = ['api', 'reference', 'class']
-
- return categories
-
- def generate_quick_reference(self, pages: List[Dict[str, Any]]) -> List[Dict[str, str]]:
- """Generate quick reference from common patterns (NEW FEATURE)"""
- quick_ref = []
-
- # Collect all patterns
- all_patterns = []
- for page in pages:
- all_patterns.extend(page.get('patterns', []))
-
- # Get most common code patterns
- seen_codes = set()
- for pattern in all_patterns:
- code = pattern['code']
- if code not in seen_codes and len(code) < 300:
- quick_ref.append(pattern)
- seen_codes.add(code)
- if len(quick_ref) >= 15:
- break
-
- return quick_ref
-
- def create_reference_file(self, category: str, pages: List[Dict[str, Any]]) -> None:
- """Create enhanced reference file"""
- if not pages:
- return
-
- lines = []
- lines.append(f"# {self.name.title()} - {category.replace('_', ' ').title()}\n")
- lines.append(f"**Pages:** {len(pages)}\n")
- lines.append("---\n")
-
- for page in pages:
- lines.append(f"## {page['title']}\n")
- lines.append(f"**URL:** {page['url']}\n")
-
- # Table of contents from headings
- if page.get('headings'):
- lines.append("**Contents:**")
- for h in page['headings'][:10]:
- level = int(h['level'][1]) if len(h['level']) > 1 else 1
- indent = " " * max(0, level - 2)
- lines.append(f"{indent}- {h['text']}")
- lines.append("")
-
- # Content (NO TRUNCATION)
- if page.get('content'):
- lines.append(page['content'])
- lines.append("")
-
- # Code examples with language (NO TRUNCATION)
- if page.get('code_samples'):
- lines.append("**Examples:**\n")
- for i, sample in enumerate(page['code_samples'][:4], 1):
- lang = sample.get('language', 'unknown')
- code = sample.get('code', sample if isinstance(sample, str) else '')
- lines.append(f"Example {i} ({lang}):")
- lines.append(f"```{lang}")
- lines.append(code) # Full code, no truncation
- lines.append("```\n")
-
- lines.append("---\n")
-
- filepath = os.path.join(self.skill_dir, "references", f"{category}.md")
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- logger.info(" ✓ %s.md (%d pages)", category, len(pages))
-
- def create_enhanced_skill_md(self, categories: Dict[str, List[Dict[str, Any]]], quick_ref: List[Dict[str, str]]) -> None:
- """Create SKILL.md with actual examples (IMPROVED)"""
- description = self.config.get('description', f'Comprehensive assistance with {self.name}')
-
- # Extract actual code examples from docs
- example_codes = []
- for pages in categories.values():
- for page in pages[:3]: # First 3 pages per category
- for sample in page.get('code_samples', [])[:2]: # First 2 samples per page
- code = sample.get('code', sample if isinstance(sample, str) else '')
- lang = sample.get('language', 'unknown')
- if len(code) < 200 and lang != 'unknown':
- example_codes.append((lang, code))
- if len(example_codes) >= 10:
- break
- if len(example_codes) >= 10:
- break
- if len(example_codes) >= 10:
- break
-
- content = f"""---
-name: {self.name}
-description: {description}
----
-
-# {self.name.title()} Skill
-
-Comprehensive assistance with {self.name} development, generated from official documentation.
-
-## When to Use This Skill
-
-This skill should be triggered when:
-- Working with {self.name}
-- Asking about {self.name} features or APIs
-- Implementing {self.name} solutions
-- Debugging {self.name} code
-- Learning {self.name} best practices
-
-## Quick Reference
-
-### Common Patterns
-
-"""
-
- # Add actual quick reference patterns
- if quick_ref:
- for i, pattern in enumerate(quick_ref[:8], 1):
- content += f"**Pattern {i}:** {pattern.get('description', 'Example pattern')}\n\n"
- content += "```\n"
- content += pattern.get('code', '')[:300]
- content += "\n```\n\n"
- else:
- content += "*Quick reference patterns will be added as you use the skill.*\n\n"
-
- # Add example codes from docs
- if example_codes:
- content += "### Example Code Patterns\n\n"
- for i, (lang, code) in enumerate(example_codes[:5], 1):
- content += f"**Example {i}** ({lang}):\n```{lang}\n{code}\n```\n\n"
-
- content += f"""## Reference Files
-
-This skill includes comprehensive documentation in `references/`:
-
-"""
-
- for cat in sorted(categories.keys()):
- content += f"- **{cat}.md** - {cat.replace('_', ' ').title()} documentation\n"
-
- content += """
-Use `view` to read specific reference files when detailed information is needed.
-
-## Working with This Skill
-
-### For Beginners
-Start with the getting_started or tutorials reference files for foundational concepts.
-
-### For Specific Features
-Use the appropriate category reference file (api, guides, etc.) for detailed information.
-
-### For Code Examples
-The quick reference section above contains common patterns extracted from the official docs.
-
-## Resources
-
-### references/
-Organized documentation extracted from official sources. These files contain:
-- Detailed explanations
-- Code examples with language annotations
-- Links to original documentation
-- Table of contents for quick navigation
-
-### scripts/
-Add helper scripts here for common automation tasks.
-
-### assets/
-Add templates, boilerplate, or example projects here.
-
-## Notes
-
-- This skill was automatically generated from official documentation
-- Reference files preserve the structure and examples from source docs
-- Code examples include language detection for better syntax highlighting
-- Quick reference patterns are extracted from common usage examples in the docs
-
-## Updating
-
-To refresh this skill with updated documentation:
-1. Re-run the scraper with the same configuration
-2. The skill will be rebuilt with the latest information
-"""
-
- filepath = os.path.join(self.skill_dir, "SKILL.md")
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(content)
-
- logger.info(" ✓ SKILL.md (enhanced with %d examples)", len(example_codes))
-
- def create_index(self, categories: Dict[str, List[Dict[str, Any]]]) -> None:
- """Create navigation index"""
- lines = []
- lines.append(f"# {self.name.title()} Documentation Index\n")
- lines.append("## Categories\n")
-
- for cat, pages in sorted(categories.items()):
- lines.append(f"### {cat.replace('_', ' ').title()}")
- lines.append(f"**File:** `{cat}.md`")
- lines.append(f"**Pages:** {len(pages)}\n")
-
- filepath = os.path.join(self.skill_dir, "references", "index.md")
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- logger.info(" ✓ index.md")
-
- def build_skill(self) -> bool:
- """Build the skill from scraped data.
-
- Loads scraped JSON files, categorizes pages, extracts patterns,
- and generates SKILL.md and reference files.
-
- Returns:
- bool: True if build succeeded, False otherwise
- """
- logger.info("\n" + "=" * 60)
- logger.info("BUILDING SKILL: %s", self.name)
- logger.info("=" * 60 + "\n")
-
- # Load data
- logger.info("Loading scraped data...")
- pages = self.load_scraped_data()
-
- if not pages:
- logger.error("✗ No scraped data found!")
- return False
-
- logger.info(" ✓ Loaded %d pages\n", len(pages))
-
- # Categorize
- logger.info("Categorizing pages...")
- categories = self.smart_categorize(pages)
- logger.info(" ✓ Created %d categories\n", len(categories))
-
- # Generate quick reference
- logger.info("Generating quick reference...")
- quick_ref = self.generate_quick_reference(pages)
- logger.info(" ✓ Extracted %d patterns\n", len(quick_ref))
-
- # Create reference files
- logger.info("Creating reference files...")
- for cat, cat_pages in categories.items():
- self.create_reference_file(cat, cat_pages)
-
- # Create index
- self.create_index(categories)
- logger.info("")
-
- # Create enhanced SKILL.md
- logger.info("Creating SKILL.md...")
- self.create_enhanced_skill_md(categories, quick_ref)
-
- logger.info("\n✅ Skill built: %s/", self.skill_dir)
- return True
-
-
-def validate_config(config: Dict[str, Any]) -> Tuple[List[str], List[str]]:
- """Validate configuration structure and values.
-
- Args:
- config (dict): Configuration dictionary to validate
-
- Returns:
- tuple: (errors, warnings) where each is a list of strings
-
- Example:
- >>> errors, warnings = validate_config({'name': 'test', 'base_url': 'https://example.com'})
- >>> if errors:
- ... print("Invalid config:", errors)
- """
- errors = []
- warnings = []
-
- # Required fields
- required_fields = ['name', 'base_url']
- for field in required_fields:
- if field not in config:
- errors.append(f"Missing required field: '{field}'")
-
- # Validate name (alphanumeric, hyphens, underscores only)
- if 'name' in config:
- if not re.match(r'^[a-zA-Z0-9_-]+$', config['name']):
- errors.append(f"Invalid name: '{config['name']}' (use only letters, numbers, hyphens, underscores)")
-
- # Validate base_url
- if 'base_url' in config:
- if not config['base_url'].startswith(('http://', 'https://')):
- errors.append(f"Invalid base_url: '{config['base_url']}' (must start with http:// or https://)")
-
- # Validate selectors structure
- if 'selectors' in config:
- if not isinstance(config['selectors'], dict):
- errors.append("'selectors' must be a dictionary")
- else:
- recommended_selectors = ['main_content', 'title', 'code_blocks']
- for selector in recommended_selectors:
- if selector not in config['selectors']:
- warnings.append(f"Missing recommended selector: '{selector}'")
- else:
- warnings.append("Missing 'selectors' section (recommended)")
-
- # Validate url_patterns
- if 'url_patterns' in config:
- if not isinstance(config['url_patterns'], dict):
- errors.append("'url_patterns' must be a dictionary")
- else:
- for key in ['include', 'exclude']:
- if key in config['url_patterns']:
- if not isinstance(config['url_patterns'][key], list):
- errors.append(f"'url_patterns.{key}' must be a list")
-
- # Validate categories
- if 'categories' in config:
- if not isinstance(config['categories'], dict):
- errors.append("'categories' must be a dictionary")
- else:
- for cat_name, keywords in config['categories'].items():
- if not isinstance(keywords, list):
- errors.append(f"'categories.{cat_name}' must be a list of keywords")
-
- # Validate rate_limit
- if 'rate_limit' in config:
- try:
- rate = float(config['rate_limit'])
- if rate < 0:
- errors.append(f"'rate_limit' must be non-negative (got {rate})")
- elif rate > 10:
- warnings.append(f"'rate_limit' is very high ({rate}s) - this may slow down scraping significantly")
- except (ValueError, TypeError):
- errors.append(f"'rate_limit' must be a number (got {config['rate_limit']})")
-
- # Validate max_pages
- if 'max_pages' in config:
- max_p_value = config['max_pages']
-
- # Allow None for unlimited
- if max_p_value is None:
- warnings.append("'max_pages' is None (unlimited) - this will scrape ALL pages. Use with caution!")
- else:
- try:
- max_p = int(max_p_value)
- # Allow -1 for unlimited
- if max_p == -1:
- warnings.append("'max_pages' is -1 (unlimited) - this will scrape ALL pages. Use with caution!")
- elif max_p < 1:
- errors.append(f"'max_pages' must be at least 1 or -1 for unlimited (got {max_p})")
- elif max_p > MAX_PAGES_WARNING_THRESHOLD:
- warnings.append(f"'max_pages' is very high ({max_p}) - scraping may take a very long time")
- except (ValueError, TypeError):
- errors.append(f"'max_pages' must be an integer, -1, or null (got {config['max_pages']})")
-
- # Validate start_urls if present
- if 'start_urls' in config:
- if not isinstance(config['start_urls'], list):
- errors.append("'start_urls' must be a list")
- else:
- for url in config['start_urls']:
- if not url.startswith(('http://', 'https://')):
- errors.append(f"Invalid start_url: '{url}' (must start with http:// or https://)")
-
- return errors, warnings
-
-
-def load_config(config_path: str) -> Dict[str, Any]:
- """Load and validate configuration from JSON file.
-
- Args:
- config_path (str): Path to JSON configuration file
-
- Returns:
- dict: Validated configuration dictionary
-
- Raises:
- SystemExit: If config is invalid or file not found
-
- Example:
- >>> config = load_config('configs/react.json')
- >>> print(config['name'])
- 'react'
- """
- try:
- with open(config_path, 'r') as f:
- config = json.load(f)
- except json.JSONDecodeError as e:
- logger.error("❌ Error: Invalid JSON in config file: %s", config_path)
- logger.error(" Details: %s", e)
- logger.error(" Suggestion: Check syntax at line %d, column %d", e.lineno, e.colno)
- sys.exit(1)
- except FileNotFoundError:
- logger.error("❌ Error: Config file not found: %s", config_path)
- logger.error(" Suggestion: Create a config file or use an existing one from configs/")
- logger.error(" Available configs: react.json, vue.json, django.json, godot.json")
- sys.exit(1)
-
- # Validate config
- errors, warnings = validate_config(config)
-
- # Show warnings (non-blocking)
- if warnings:
- logger.warning("⚠️ Configuration warnings in %s:", config_path)
- for warning in warnings:
- logger.warning(" - %s", warning)
- logger.info("")
-
- # Show errors (blocking)
- if errors:
- logger.error("❌ Configuration validation errors in %s:", config_path)
- for error in errors:
- logger.error(" - %s", error)
- logger.error("\n Suggestion: Fix the above errors or check configs/ for working examples")
- sys.exit(1)
-
- return config
-
-
-def interactive_config() -> Dict[str, Any]:
- """Interactive configuration wizard for creating new configs.
-
- Prompts user for all required configuration fields step-by-step
- and returns a complete configuration dictionary.
-
- Returns:
- dict: Complete configuration dictionary with user-provided values
-
- Example:
- >>> config = interactive_config()
- # User enters: name=react, url=https://react.dev, etc.
- >>> config['name']
- 'react'
- """
- logger.info("\n" + "="*60)
- logger.info("Documentation to Skill Converter")
- logger.info("="*60 + "\n")
-
- config: Dict[str, Any] = {}
-
- # Basic info
- config['name'] = input("Skill name (e.g., 'react', 'godot'): ").strip()
- config['description'] = input("Skill description: ").strip()
- config['base_url'] = input("Base URL (e.g., https://docs.example.com/): ").strip()
-
- if not config['base_url'].endswith('/'):
- config['base_url'] += '/'
-
- # Selectors
- logger.info("\nCSS Selectors (press Enter for defaults):")
- selectors = {}
- selectors['main_content'] = input(" Main content [div[role='main']]: ").strip() or "div[role='main']"
- selectors['title'] = input(" Title [title]: ").strip() or "title"
- selectors['code_blocks'] = input(" Code blocks [pre code]: ").strip() or "pre code"
- config['selectors'] = selectors
-
- # URL patterns
- logger.info("\nURL Patterns (comma-separated, optional):")
- include = input(" Include: ").strip()
- exclude = input(" Exclude: ").strip()
- config['url_patterns'] = {
- 'include': [p.strip() for p in include.split(',') if p.strip()],
- 'exclude': [p.strip() for p in exclude.split(',') if p.strip()]
- }
-
- # Settings
- rate = input(f"\nRate limit (seconds) [{DEFAULT_RATE_LIMIT}]: ").strip()
- config['rate_limit'] = float(rate) if rate else DEFAULT_RATE_LIMIT
-
- max_p = input(f"Max pages [{DEFAULT_MAX_PAGES}]: ").strip()
- config['max_pages'] = int(max_p) if max_p else DEFAULT_MAX_PAGES
-
- return config
-
-
-def check_existing_data(name: str) -> Tuple[bool, int]:
- """Check if scraped data already exists for a skill.
-
- Args:
- name (str): Skill name to check
-
- Returns:
- tuple: (exists, page_count) where exists is bool and page_count is int
-
- Example:
- >>> exists, count = check_existing_data('react')
- >>> if exists:
- ... print(f"Found {count} existing pages")
- """
- data_dir = f"output/{name}_data"
- if os.path.exists(data_dir) and os.path.exists(f"{data_dir}/summary.json"):
- with open(f"{data_dir}/summary.json", 'r') as f:
- summary = json.load(f)
- return True, summary.get('total_pages', 0)
- return False, 0
-
-
-def setup_argument_parser() -> argparse.ArgumentParser:
- """Setup and configure command-line argument parser.
-
- Creates an ArgumentParser with all CLI options for the doc scraper tool,
- including configuration, scraping, enhancement, and performance options.
-
- Returns:
- argparse.ArgumentParser: Configured argument parser
-
- Example:
- >>> parser = setup_argument_parser()
- >>> args = parser.parse_args(['--config', 'configs/react.json'])
- >>> print(args.config)
- configs/react.json
- """
- parser = argparse.ArgumentParser(
- description='Convert documentation websites to Claude skills',
- formatter_class=argparse.RawDescriptionHelpFormatter
- )
-
- parser.add_argument('--interactive', '-i', action='store_true',
- help='Interactive configuration mode')
- parser.add_argument('--config', '-c', type=str,
- help='Load configuration from file (e.g., configs/godot.json)')
- parser.add_argument('--name', type=str,
- help='Skill name')
- parser.add_argument('--url', type=str,
- help='Base documentation URL')
- parser.add_argument('--description', '-d', type=str,
- help='Skill description')
- parser.add_argument('--skip-scrape', action='store_true',
- help='Skip scraping, use existing data')
- parser.add_argument('--dry-run', action='store_true',
- help='Preview what will be scraped without actually scraping')
- parser.add_argument('--enhance', action='store_true',
- help='Enhance SKILL.md using Claude API after building (requires API key)')
- parser.add_argument('--enhance-local', action='store_true',
- help='Enhance SKILL.md using Claude Code (no API key needed, runs in background)')
- parser.add_argument('--interactive-enhancement', action='store_true',
- help='Open terminal window for enhancement (use with --enhance-local)')
- parser.add_argument('--api-key', type=str,
- help='Anthropic API key for --enhance (or set ANTHROPIC_API_KEY)')
- parser.add_argument('--resume', action='store_true',
- help='Resume from last checkpoint (for interrupted scrapes)')
- parser.add_argument('--fresh', action='store_true',
- help='Clear checkpoint and start fresh')
- parser.add_argument('--rate-limit', '-r', type=float, metavar='SECONDS',
- help=f'Override rate limit in seconds (default: from config or {DEFAULT_RATE_LIMIT}). Use 0 for no delay.')
- parser.add_argument('--workers', '-w', type=int, metavar='N',
- help='Number of parallel workers for faster scraping (default: 1, max: 10)')
- parser.add_argument('--async', dest='async_mode', action='store_true',
- help='Enable async mode for better parallel performance (2-3x faster than threads)')
- parser.add_argument('--no-rate-limit', action='store_true',
- help='Disable rate limiting completely (same as --rate-limit 0)')
- parser.add_argument('--verbose', '-v', action='store_true',
- help='Enable verbose output (DEBUG level logging)')
- parser.add_argument('--quiet', '-q', action='store_true',
- help='Minimize output (WARNING level logging only)')
-
- return parser
-
-
-def get_configuration(args: argparse.Namespace) -> Dict[str, Any]:
- """Load or create configuration from command-line arguments.
-
- Handles three configuration modes:
- 1. Load from JSON file (--config)
- 2. Interactive configuration wizard (--interactive or missing args)
- 3. Quick mode from command-line arguments (--name, --url)
-
- Also applies CLI overrides for rate limiting and worker count.
-
- Args:
- args: Parsed command-line arguments from argparse
-
- Returns:
- dict: Configuration dictionary with all required fields
-
- Example:
- >>> args = parser.parse_args(['--name', 'react', '--url', 'https://react.dev'])
- >>> config = get_configuration(args)
- >>> print(config['name'])
- react
- """
- # Get base configuration
- if args.config:
- config = load_config(args.config)
- elif args.interactive or not (args.name and args.url):
- config = interactive_config()
- else:
- config = {
- 'name': args.name,
- 'description': args.description or f'Comprehensive assistance with {args.name}',
- 'base_url': args.url,
- 'selectors': {
- 'main_content': "div[role='main']",
- 'title': 'title',
- 'code_blocks': 'pre code'
- },
- 'url_patterns': {'include': [], 'exclude': []},
- 'rate_limit': DEFAULT_RATE_LIMIT,
- 'max_pages': DEFAULT_MAX_PAGES
- }
-
- # Apply CLI overrides for rate limiting
- if args.no_rate_limit:
- config['rate_limit'] = 0
- logger.info("⚡ Rate limiting disabled")
- elif args.rate_limit is not None:
- config['rate_limit'] = args.rate_limit
- if args.rate_limit == 0:
- logger.info("⚡ Rate limiting disabled")
- else:
- logger.info("⚡ Rate limit override: %ss per page", args.rate_limit)
-
- # Apply CLI overrides for worker count
- if args.workers:
- # Validate workers count
- if args.workers < 1:
- logger.error("❌ Error: --workers must be at least 1 (got %d)", args.workers)
- logger.error(" Suggestion: Use --workers 1 (default) or omit the flag")
- sys.exit(1)
- if args.workers > 10:
- logger.warning("⚠️ Warning: --workers capped at 10 (requested %d)", args.workers)
- args.workers = 10
- config['workers'] = args.workers
- if args.workers > 1:
- logger.info("🚀 Parallel scraping enabled: %d workers", args.workers)
-
- # Apply CLI override for async mode
- if args.async_mode:
- config['async_mode'] = True
- if config.get('workers', 1) > 1:
- logger.info("⚡ Async mode enabled (2-3x faster than threads)")
- else:
- logger.warning("⚠️ Async mode enabled but workers=1. Consider using --workers 4 for better performance")
-
- return config
-
-
-def execute_scraping_and_building(config: Dict[str, Any], args: argparse.Namespace) -> Optional['DocToSkillConverter']:
- """Execute the scraping and skill building process.
-
- Handles dry run mode, existing data checks, scraping with checkpoints,
- keyboard interrupts, and skill building. This is the core workflow
- orchestration for the scraping phase.
-
- Args:
- config (dict): Configuration dictionary with scraping parameters
- args: Parsed command-line arguments
-
- Returns:
- DocToSkillConverter: The converter instance after scraping/building,
- or None if process was aborted
-
- Example:
- >>> config = {'name': 'react', 'base_url': 'https://react.dev'}
- >>> converter = execute_scraping_and_building(config, args)
- >>> if converter:
- ... print("Scraping complete!")
- """
- # Dry run mode - preview only
- if args.dry_run:
- logger.info("\n" + "=" * 60)
- logger.info("DRY RUN MODE")
- logger.info("=" * 60)
- logger.info("This will show what would be scraped without saving anything.\n")
-
- converter = DocToSkillConverter(config, dry_run=True)
- converter.scrape_all()
-
- logger.info("\n📋 Configuration Summary:")
- logger.info(" Name: %s", config['name'])
- logger.info(" Base URL: %s", config['base_url'])
- logger.info(" Max pages: %d", config.get('max_pages', DEFAULT_MAX_PAGES))
- logger.info(" Rate limit: %ss", config.get('rate_limit', DEFAULT_RATE_LIMIT))
- logger.info(" Categories: %d", len(config.get('categories', {})))
- return None
-
- # Check for existing data
- exists, page_count = check_existing_data(config['name'])
-
- if exists and not args.skip_scrape and not args.fresh:
- # Check force_rescrape flag from config
- if config.get('force_rescrape', False):
- # Auto-delete cached data and rescrape
- logger.info("\n✓ Found existing data: %d pages", page_count)
- logger.info(" force_rescrape enabled - deleting cached data and rescaping")
- import shutil
- data_dir = f"output/{config['name']}_data"
- if os.path.exists(data_dir):
- shutil.rmtree(data_dir)
- logger.info(f" Deleted: {data_dir}")
- else:
- # Only prompt if force_rescrape is False
- logger.info("\n✓ Found existing data: %d pages", page_count)
- response = input("Use existing data? (y/n): ").strip().lower()
- if response == 'y':
- args.skip_scrape = True
- elif exists and args.fresh:
- logger.info("\n✓ Found existing data: %d pages", page_count)
- logger.info(" --fresh flag set, will re-scrape from scratch")
-
- # Create converter
- converter = DocToSkillConverter(config, resume=args.resume)
-
- # Handle fresh start (clear checkpoint)
- if args.fresh:
- converter.clear_checkpoint()
-
- # Scrape or skip
- if not args.skip_scrape:
- try:
- converter.scrape_all()
- # Save final checkpoint
- if converter.checkpoint_enabled:
- converter.save_checkpoint()
- logger.info("\n💾 Final checkpoint saved")
- # Clear checkpoint after successful completion
- converter.clear_checkpoint()
- logger.info("✅ Scraping complete - checkpoint cleared")
- except KeyboardInterrupt:
- logger.warning("\n\nScraping interrupted.")
- if converter.checkpoint_enabled:
- converter.save_checkpoint()
- logger.info("💾 Progress saved to checkpoint")
- logger.info(" Resume with: --config %s --resume", args.config if args.config else 'config.json')
- response = input("Continue with skill building? (y/n): ").strip().lower()
- if response != 'y':
- return None
- else:
- logger.info("\n⏭️ Skipping scrape, using existing data")
-
- # Build skill
- success = converter.build_skill()
-
- if not success:
- sys.exit(1)
-
- return converter
-
-
-def execute_enhancement(config: Dict[str, Any], args: argparse.Namespace) -> None:
- """Execute optional SKILL.md enhancement with Claude.
-
- Supports two enhancement modes:
- 1. API-based enhancement (requires ANTHROPIC_API_KEY)
- 2. Local enhancement using Claude Code (no API key needed)
-
- Prints appropriate messages and suggestions based on whether
- enhancement was requested and whether it succeeded.
-
- Args:
- config (dict): Configuration dictionary with skill name
- args: Parsed command-line arguments with enhancement flags
-
- Example:
- >>> execute_enhancement(config, args)
- # Runs enhancement if --enhance or --enhance-local flag is set
- """
- import subprocess
-
- # Optional enhancement with Claude API
- if args.enhance:
- logger.info("\n" + "=" * 60)
- logger.info("ENHANCING SKILL.MD WITH CLAUDE API")
- logger.info("=" * 60 + "\n")
-
- try:
- enhance_cmd = ['python3', 'cli/enhance_skill.py', f'output/{config["name"]}/']
- if args.api_key:
- enhance_cmd.extend(['--api-key', args.api_key])
-
- result = subprocess.run(enhance_cmd, check=True)
- if result.returncode == 0:
- logger.info("\n✅ Enhancement complete!")
- except subprocess.CalledProcessError:
- logger.warning("\n⚠ Enhancement failed, but skill was still built")
- except FileNotFoundError:
- logger.warning("\n⚠ enhance_skill.py not found. Run manually:")
- logger.info(" skill-seekers-enhance output/%s/", config['name'])
-
- # Optional enhancement with Claude Code (local, no API key)
- if args.enhance_local:
- logger.info("\n" + "=" * 60)
- if args.interactive_enhancement:
- logger.info("ENHANCING SKILL.MD WITH CLAUDE CODE (INTERACTIVE)")
- else:
- logger.info("ENHANCING SKILL.MD WITH CLAUDE CODE (HEADLESS)")
- logger.info("=" * 60 + "\n")
-
- try:
- enhance_cmd = ['skill-seekers-enhance', f'output/{config["name"]}/']
- if args.interactive_enhancement:
- enhance_cmd.append('--interactive-enhancement')
-
- result = subprocess.run(enhance_cmd, check=True)
-
- if result.returncode == 0:
- logger.info("\n✅ Enhancement complete!")
- except subprocess.CalledProcessError:
- logger.warning("\n⚠ Enhancement failed, but skill was still built")
- except FileNotFoundError:
- logger.warning("\n⚠ skill-seekers-enhance command not found. Run manually:")
- logger.info(" skill-seekers-enhance output/%s/", config['name'])
-
- # Print packaging instructions
- logger.info("\n📦 Package your skill:")
- logger.info(" skill-seekers-package output/%s/", config['name'])
-
- # Suggest enhancement if not done
- if not args.enhance and not args.enhance_local:
- logger.info("\n💡 Optional: Enhance SKILL.md with Claude:")
- logger.info(" Local (recommended): skill-seekers-enhance output/%s/", config['name'])
- logger.info(" or re-run with: --enhance-local")
- logger.info(" API-based: skill-seekers-enhance-api output/%s/", config['name'])
- logger.info(" or re-run with: --enhance")
- logger.info("\n💡 Tip: Use --interactive-enhancement with --enhance-local to open terminal window")
-
-
-def main() -> None:
- parser = setup_argument_parser()
- args = parser.parse_args()
-
- # Setup logging based on verbosity flags
- setup_logging(verbose=args.verbose, quiet=args.quiet)
-
- config = get_configuration(args)
-
- # Execute scraping and building
- converter = execute_scraping_and_building(config, args)
-
- # Exit if dry run or aborted
- if converter is None:
- return
-
- # Execute enhancement and print instructions
- execute_enhancement(config, args)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill.py
deleted file mode 100644
index 50df45b..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill.py
+++ /dev/null
@@ -1,273 +0,0 @@
-#!/usr/bin/env python3
-"""
-SKILL.md Enhancement Script
-Uses Claude API to improve SKILL.md by analyzing reference documentation.
-
-Usage:
- skill-seekers enhance output/steam-inventory/
- skill-seekers enhance output/react/
- skill-seekers enhance output/godot/ --api-key YOUR_API_KEY
-"""
-
-import os
-import sys
-import json
-import argparse
-from pathlib import Path
-
-# Add parent directory to path for imports when run as script
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.constants import API_CONTENT_LIMIT, API_PREVIEW_LIMIT
-from skill_seekers.cli.utils import read_reference_files
-
-try:
- import anthropic
-except ImportError:
- print("❌ Error: anthropic package not installed")
- print("Install with: pip3 install anthropic")
- sys.exit(1)
-
-
-class SkillEnhancer:
- def __init__(self, skill_dir, api_key=None):
- self.skill_dir = Path(skill_dir)
- self.references_dir = self.skill_dir / "references"
- self.skill_md_path = self.skill_dir / "SKILL.md"
-
- # Get API key
- self.api_key = api_key or os.environ.get('ANTHROPIC_API_KEY')
- if not self.api_key:
- raise ValueError(
- "No API key provided. Set ANTHROPIC_API_KEY environment variable "
- "or use --api-key argument"
- )
-
- self.client = anthropic.Anthropic(api_key=self.api_key)
-
- def read_current_skill_md(self):
- """Read existing SKILL.md"""
- if not self.skill_md_path.exists():
- return None
- return self.skill_md_path.read_text(encoding='utf-8')
-
- def enhance_skill_md(self, references, current_skill_md):
- """Use Claude to enhance SKILL.md"""
-
- # Build prompt
- prompt = self._build_enhancement_prompt(references, current_skill_md)
-
- print("\n🤖 Asking Claude to enhance SKILL.md...")
- print(f" Input: {len(prompt):,} characters")
-
- try:
- message = self.client.messages.create(
- model="claude-sonnet-4-20250514",
- max_tokens=4096,
- temperature=0.3,
- messages=[{
- "role": "user",
- "content": prompt
- }]
- )
-
- enhanced_content = message.content[0].text
- return enhanced_content
-
- except Exception as e:
- print(f"❌ Error calling Claude API: {e}")
- return None
-
- def _build_enhancement_prompt(self, references, current_skill_md):
- """Build the prompt for Claude"""
-
- # Extract skill name and description
- skill_name = self.skill_dir.name
-
- prompt = f"""You are enhancing a Claude skill's SKILL.md file. This skill is about: {skill_name}
-
-I've scraped documentation and organized it into reference files. Your job is to create an EXCELLENT SKILL.md that will help Claude use this documentation effectively.
-
-CURRENT SKILL.MD:
-{'```markdown' if current_skill_md else '(none - create from scratch)'}
-{current_skill_md or 'No existing SKILL.md'}
-{'```' if current_skill_md else ''}
-
-REFERENCE DOCUMENTATION:
-"""
-
- for filename, content in references.items():
- prompt += f"\n\n## {filename}\n```markdown\n{content[:30000]}\n```\n"
-
- prompt += """
-
-YOUR TASK:
-Create an enhanced SKILL.md that includes:
-
-1. **Clear "When to Use This Skill" section** - Be specific about trigger conditions
-2. **Excellent Quick Reference section** - Extract 5-10 of the BEST, most practical code examples from the reference docs
- - Choose SHORT, clear examples that demonstrate common tasks
- - Include both simple and intermediate examples
- - Annotate examples with clear descriptions
- - Use proper language tags (cpp, python, javascript, json, etc.)
-3. **Detailed Reference Files description** - Explain what's in each reference file
-4. **Practical "Working with This Skill" section** - Give users clear guidance on how to navigate the skill
-5. **Key Concepts section** (if applicable) - Explain core concepts
-6. **Keep the frontmatter** (---\nname: ...\n---) intact
-
-IMPORTANT:
-- Extract REAL examples from the reference docs, don't make them up
-- Prioritize SHORT, clear examples (5-20 lines max)
-- Make it actionable and practical
-- Don't be too verbose - be concise but useful
-- Maintain the markdown structure for Claude skills
-- Keep code examples properly formatted with language tags
-
-OUTPUT:
-Return ONLY the complete SKILL.md content, starting with the frontmatter (---).
-"""
-
- return prompt
-
- def save_enhanced_skill_md(self, content):
- """Save the enhanced SKILL.md"""
- # Backup original
- if self.skill_md_path.exists():
- backup_path = self.skill_md_path.with_suffix('.md.backup')
- self.skill_md_path.rename(backup_path)
- print(f" 💾 Backed up original to: {backup_path.name}")
-
- # Save enhanced version
- self.skill_md_path.write_text(content, encoding='utf-8')
- print(f" ✅ Saved enhanced SKILL.md")
-
- def run(self):
- """Main enhancement workflow"""
- print(f"\n{'='*60}")
- print(f"ENHANCING SKILL: {self.skill_dir.name}")
- print(f"{'='*60}\n")
-
- # Read reference files
- print("📖 Reading reference documentation...")
- references = read_reference_files(
- self.skill_dir,
- max_chars=API_CONTENT_LIMIT,
- preview_limit=API_PREVIEW_LIMIT
- )
-
- if not references:
- print("❌ No reference files found to analyze")
- return False
-
- print(f" ✓ Read {len(references)} reference files")
- total_size = sum(len(c) for c in references.values())
- print(f" ✓ Total size: {total_size:,} characters\n")
-
- # Read current SKILL.md
- current_skill_md = self.read_current_skill_md()
- if current_skill_md:
- print(f" ℹ Found existing SKILL.md ({len(current_skill_md)} chars)")
- else:
- print(f" ℹ No existing SKILL.md, will create new one")
-
- # Enhance with Claude
- enhanced = self.enhance_skill_md(references, current_skill_md)
-
- if not enhanced:
- print("❌ Enhancement failed")
- return False
-
- print(f" ✓ Generated enhanced SKILL.md ({len(enhanced)} chars)\n")
-
- # Save
- print("💾 Saving enhanced SKILL.md...")
- self.save_enhanced_skill_md(enhanced)
-
- print(f"\n✅ Enhancement complete!")
- print(f"\nNext steps:")
- print(f" 1. Review: {self.skill_md_path}")
- print(f" 2. If you don't like it, restore backup: {self.skill_md_path.with_suffix('.md.backup')}")
- print(f" 3. Package your skill:")
- print(f" skill-seekers package {self.skill_dir}/")
-
- return True
-
-
-def main():
- parser = argparse.ArgumentParser(
- description='Enhance SKILL.md using Claude API',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Using ANTHROPIC_API_KEY environment variable
- export ANTHROPIC_API_KEY=sk-ant-...
- skill-seekers enhance output/steam-inventory/
-
- # Providing API key directly
- skill-seekers enhance output/react/ --api-key sk-ant-...
-
- # Show what would be done (dry run)
- skill-seekers enhance output/godot/ --dry-run
-"""
- )
-
- parser.add_argument('skill_dir', type=str,
- help='Path to skill directory (e.g., output/steam-inventory/)')
- parser.add_argument('--api-key', type=str,
- help='Anthropic API key (or set ANTHROPIC_API_KEY env var)')
- parser.add_argument('--dry-run', action='store_true',
- help='Show what would be done without calling API')
-
- args = parser.parse_args()
-
- # Validate skill directory
- skill_dir = Path(args.skill_dir)
- if not skill_dir.exists():
- print(f"❌ Error: Directory not found: {skill_dir}")
- sys.exit(1)
-
- if not skill_dir.is_dir():
- print(f"❌ Error: Not a directory: {skill_dir}")
- sys.exit(1)
-
- # Dry run mode
- if args.dry_run:
- print(f"🔍 DRY RUN MODE")
- print(f" Would enhance: {skill_dir}")
- print(f" References: {skill_dir / 'references'}")
- print(f" SKILL.md: {skill_dir / 'SKILL.md'}")
-
- refs_dir = skill_dir / "references"
- if refs_dir.exists():
- ref_files = list(refs_dir.glob("*.md"))
- print(f" Found {len(ref_files)} reference files:")
- for rf in ref_files:
- size = rf.stat().st_size
- print(f" - {rf.name} ({size:,} bytes)")
-
- print("\nTo actually run enhancement:")
- print(f" skill-seekers enhance {skill_dir}")
- return
-
- # Create enhancer and run
- try:
- enhancer = SkillEnhancer(skill_dir, api_key=args.api_key)
- success = enhancer.run()
- sys.exit(0 if success else 1)
-
- except ValueError as e:
- print(f"❌ Error: {e}")
- print("\nSet your API key:")
- print(" export ANTHROPIC_API_KEY=sk-ant-...")
- print("Or provide it directly:")
- print(f" skill-seekers enhance {skill_dir} --api-key sk-ant-...")
- sys.exit(1)
- except Exception as e:
- print(f"❌ Unexpected error: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill_local.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill_local.py
deleted file mode 100644
index 99480c5..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/enhance_skill_local.py
+++ /dev/null
@@ -1,451 +0,0 @@
-#!/usr/bin/env python3
-"""
-SKILL.md Enhancement Script (Local - Using Claude Code)
-Opens a new terminal with Claude Code to enhance SKILL.md, then reports back.
-No API key needed - uses your existing Claude Code Max plan!
-
-Usage:
- skill-seekers enhance output/steam-inventory/
- skill-seekers enhance output/react/
-
-Terminal Selection:
- The script automatically detects which terminal app to use:
- 1. SKILL_SEEKER_TERMINAL env var (highest priority)
- Example: export SKILL_SEEKER_TERMINAL="Ghostty"
- 2. TERM_PROGRAM env var (current terminal)
- 3. Terminal.app (fallback)
-
- Supported terminals: Ghostty, iTerm, Terminal, WezTerm
-"""
-
-import os
-import sys
-import time
-import subprocess
-import tempfile
-from pathlib import Path
-
-# Add parent directory to path for imports when run as script
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.constants import LOCAL_CONTENT_LIMIT, LOCAL_PREVIEW_LIMIT
-from skill_seekers.cli.utils import read_reference_files
-
-
-def detect_terminal_app():
- """Detect which terminal app to use with cascading priority.
-
- Priority order:
- 1. SKILL_SEEKER_TERMINAL environment variable (explicit user preference)
- 2. TERM_PROGRAM environment variable (inherit current terminal)
- 3. Terminal.app (fallback default)
-
- Returns:
- tuple: (terminal_app_name, detection_method)
- - terminal_app_name (str): Name of terminal app to launch (e.g., "Ghostty", "Terminal")
- - detection_method (str): How the terminal was detected (for logging)
-
- Examples:
- >>> os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
- >>> detect_terminal_app()
- ('Ghostty', 'SKILL_SEEKER_TERMINAL')
-
- >>> os.environ['TERM_PROGRAM'] = 'iTerm.app'
- >>> detect_terminal_app()
- ('iTerm', 'TERM_PROGRAM')
- """
- # Map TERM_PROGRAM values to macOS app names
- TERMINAL_MAP = {
- 'Apple_Terminal': 'Terminal',
- 'iTerm.app': 'iTerm',
- 'ghostty': 'Ghostty',
- 'WezTerm': 'WezTerm',
- }
-
- # Priority 1: Check SKILL_SEEKER_TERMINAL env var (explicit preference)
- preferred_terminal = os.environ.get('SKILL_SEEKER_TERMINAL', '').strip()
- if preferred_terminal:
- return preferred_terminal, 'SKILL_SEEKER_TERMINAL'
-
- # Priority 2: Check TERM_PROGRAM (inherit current terminal)
- term_program = os.environ.get('TERM_PROGRAM', '').strip()
- if term_program and term_program in TERMINAL_MAP:
- return TERMINAL_MAP[term_program], 'TERM_PROGRAM'
-
- # Priority 3: Fallback to Terminal.app
- if term_program:
- # TERM_PROGRAM is set but unknown
- return 'Terminal', f'unknown TERM_PROGRAM ({term_program})'
- else:
- # No TERM_PROGRAM set
- return 'Terminal', 'default'
-
-
-class LocalSkillEnhancer:
- def __init__(self, skill_dir):
- self.skill_dir = Path(skill_dir)
- self.references_dir = self.skill_dir / "references"
- self.skill_md_path = self.skill_dir / "SKILL.md"
-
- def create_enhancement_prompt(self):
- """Create the prompt file for Claude Code"""
-
- # Read reference files
- references = read_reference_files(
- self.skill_dir,
- max_chars=LOCAL_CONTENT_LIMIT,
- preview_limit=LOCAL_PREVIEW_LIMIT
- )
-
- if not references:
- print("❌ No reference files found")
- return None
-
- # Read current SKILL.md
- current_skill_md = ""
- if self.skill_md_path.exists():
- current_skill_md = self.skill_md_path.read_text(encoding='utf-8')
-
- # Build prompt
- prompt = f"""I need you to enhance the SKILL.md file for the {self.skill_dir.name} skill.
-
-CURRENT SKILL.MD:
-{'-'*60}
-{current_skill_md if current_skill_md else '(No existing SKILL.md - create from scratch)'}
-{'-'*60}
-
-REFERENCE DOCUMENTATION:
-{'-'*60}
-"""
-
- for filename, content in references.items():
- prompt += f"\n## {filename}\n{content[:15000]}\n"
-
- prompt += f"""
-{'-'*60}
-
-YOUR TASK:
-Create an EXCELLENT SKILL.md file that will help Claude use this documentation effectively.
-
-Requirements:
-1. **Clear "When to Use This Skill" section**
- - Be SPECIFIC about trigger conditions
- - List concrete use cases
-
-2. **Excellent Quick Reference section**
- - Extract 5-10 of the BEST, most practical code examples from the reference docs
- - Choose SHORT, clear examples (5-20 lines max)
- - Include both simple and intermediate examples
- - Use proper language tags (cpp, python, javascript, json, etc.)
- - Add clear descriptions for each example
-
-3. **Detailed Reference Files description**
- - Explain what's in each reference file
- - Help users navigate the documentation
-
-4. **Practical "Working with This Skill" section**
- - Clear guidance for beginners, intermediate, and advanced users
- - Navigation tips
-
-5. **Key Concepts section** (if applicable)
- - Explain core concepts
- - Define important terminology
-
-IMPORTANT:
-- Extract REAL examples from the reference docs above
-- Prioritize SHORT, clear examples
-- Make it actionable and practical
-- Keep the frontmatter (---\\nname: ...\\n---) intact
-- Use proper markdown formatting
-
-SAVE THE RESULT:
-Save the complete enhanced SKILL.md to: {self.skill_md_path.absolute()}
-
-First, backup the original to: {self.skill_md_path.with_suffix('.md.backup').absolute()}
-"""
-
- return prompt
-
- def run(self, headless=True, timeout=600):
- """Main enhancement workflow
-
- Args:
- headless: If True, run claude directly without opening terminal (default: True)
- timeout: Maximum time to wait for enhancement in seconds (default: 600 = 10 minutes)
- """
- print(f"\n{'='*60}")
- print(f"LOCAL ENHANCEMENT: {self.skill_dir.name}")
- print(f"{'='*60}\n")
-
- # Validate
- if not self.skill_dir.exists():
- print(f"❌ Directory not found: {self.skill_dir}")
- return False
-
- # Read reference files
- print("📖 Reading reference documentation...")
- references = read_reference_files(
- self.skill_dir,
- max_chars=LOCAL_CONTENT_LIMIT,
- preview_limit=LOCAL_PREVIEW_LIMIT
- )
-
- if not references:
- print("❌ No reference files found to analyze")
- return False
-
- print(f" ✓ Read {len(references)} reference files")
- total_size = sum(len(c) for c in references.values())
- print(f" ✓ Total size: {total_size:,} characters\n")
-
- # Create prompt
- print("📝 Creating enhancement prompt...")
- prompt = self.create_enhancement_prompt()
-
- if not prompt:
- return False
-
- # Save prompt to temp file
- with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
- prompt_file = f.name
- f.write(prompt)
-
- print(f" ✓ Prompt saved ({len(prompt):,} characters)\n")
-
- # Headless mode: Run claude directly without opening terminal
- if headless:
- return self._run_headless(prompt_file, timeout)
-
- # Terminal mode: Launch Claude Code in new terminal
- print("🚀 Launching Claude Code in new terminal...")
- print(" This will:")
- print(" 1. Open a new terminal window")
- print(" 2. Run Claude Code with the enhancement task")
- print(" 3. Claude will read the docs and enhance SKILL.md")
- print(" 4. Terminal will auto-close when done")
- print()
-
- # Create a shell script to run in the terminal
- shell_script = f'''#!/bin/bash
-claude {prompt_file}
-echo ""
-echo "✅ Enhancement complete!"
-echo "Press any key to close..."
-read -n 1
-rm {prompt_file}
-'''
-
- # Save shell script
- with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
- script_file = f.name
- f.write(shell_script)
-
- os.chmod(script_file, 0o755)
-
- # Launch in new terminal (macOS specific)
- if sys.platform == 'darwin':
- # Detect which terminal app to use
- terminal_app, detection_method = detect_terminal_app()
-
- # Show detection info
- if detection_method == 'SKILL_SEEKER_TERMINAL':
- print(f" Using terminal: {terminal_app} (from SKILL_SEEKER_TERMINAL)")
- elif detection_method == 'TERM_PROGRAM':
- print(f" Using terminal: {terminal_app} (inherited from current terminal)")
- elif detection_method.startswith('unknown TERM_PROGRAM'):
- print(f"⚠️ {detection_method}")
- print(f" → Using Terminal.app as fallback")
- else:
- print(f" Using terminal: {terminal_app} (default)")
-
- try:
- subprocess.Popen(['open', '-a', terminal_app, script_file])
- except Exception as e:
- print(f"⚠️ Error launching {terminal_app}: {e}")
- print(f"\nManually run: {script_file}")
- return False
- else:
- print("⚠️ Auto-launch only works on macOS")
- print(f"\nManually run this command in a new terminal:")
- print(f" claude '{prompt_file}'")
- print(f"\nThen delete the prompt file:")
- print(f" rm '{prompt_file}'")
- return False
-
- print("✅ New terminal launched with Claude Code!")
- print()
- print("📊 Status:")
- print(f" - Prompt file: {prompt_file}")
- print(f" - Skill directory: {self.skill_dir.absolute()}")
- print(f" - SKILL.md will be saved to: {self.skill_md_path.absolute()}")
- print(f" - Original backed up to: {self.skill_md_path.with_suffix('.md.backup').absolute()}")
- print()
- print("⏳ Wait for Claude Code to finish in the other terminal...")
- print(" (Usually takes 30-60 seconds)")
- print()
- print("💡 When done:")
- print(f" 1. Check the enhanced SKILL.md: {self.skill_md_path}")
- print(f" 2. If you don't like it, restore: mv {self.skill_md_path.with_suffix('.md.backup')} {self.skill_md_path}")
- print(f" 3. Package: skill-seekers package {self.skill_dir}/")
-
- return True
-
- def _run_headless(self, prompt_file, timeout):
- """Run Claude enhancement in headless mode (no terminal window)
-
- Args:
- prompt_file: Path to prompt file
- timeout: Maximum seconds to wait
-
- Returns:
- bool: True if enhancement succeeded
- """
- import time
- from pathlib import Path
-
- print("✨ Running Claude Code enhancement (headless mode)...")
- print(f" Timeout: {timeout} seconds ({timeout//60} minutes)")
- print()
-
- # Record initial state
- initial_mtime = self.skill_md_path.stat().st_mtime if self.skill_md_path.exists() else 0
- initial_size = self.skill_md_path.stat().st_size if self.skill_md_path.exists() else 0
-
- # Start timer
- start_time = time.time()
-
- try:
- # Run claude command directly (this WAITS for completion)
- print(" Running: claude {prompt_file}")
- print(" ⏳ Please wait...")
- print()
-
- result = subprocess.run(
- ['claude', prompt_file],
- capture_output=True,
- text=True,
- timeout=timeout
- )
-
- elapsed = time.time() - start_time
-
- # Check if successful
- if result.returncode == 0:
- # Verify SKILL.md was actually updated
- if self.skill_md_path.exists():
- new_mtime = self.skill_md_path.stat().st_mtime
- new_size = self.skill_md_path.stat().st_size
-
- if new_mtime > initial_mtime and new_size > initial_size:
- print(f"✅ Enhancement complete! ({elapsed:.1f} seconds)")
- print(f" SKILL.md updated: {new_size:,} bytes")
- print()
-
- # Clean up prompt file
- try:
- os.unlink(prompt_file)
- except:
- pass
-
- return True
- else:
- print(f"⚠️ Claude finished but SKILL.md was not updated")
- print(f" This might indicate an error during enhancement")
- print()
- return False
- else:
- print(f"❌ SKILL.md not found after enhancement")
- return False
- else:
- print(f"❌ Claude Code returned error (exit code: {result.returncode})")
- if result.stderr:
- print(f" Error: {result.stderr[:200]}")
- return False
-
- except subprocess.TimeoutExpired:
- elapsed = time.time() - start_time
- print(f"\n⚠️ Enhancement timed out after {elapsed:.0f} seconds")
- print(f" Timeout limit: {timeout} seconds")
- print()
- print(" Possible reasons:")
- print(" - Skill is very large (many references)")
- print(" - Claude is taking longer than usual")
- print(" - Network issues")
- print()
- print(" Try:")
- print(" 1. Use terminal mode: --interactive-enhancement")
- print(" 2. Reduce reference content")
- print(" 3. Try again later")
-
- # Clean up
- try:
- os.unlink(prompt_file)
- except:
- pass
-
- return False
-
- except FileNotFoundError:
- print("❌ 'claude' command not found")
- print()
- print(" Make sure Claude Code CLI is installed:")
- print(" See: https://docs.claude.com/claude-code")
- print()
- print(" Try terminal mode instead: --interactive-enhancement")
-
- return False
-
- except Exception as e:
- print(f"❌ Unexpected error: {e}")
- return False
-
-
-def main():
- import argparse
-
- parser = argparse.ArgumentParser(
- description="Enhance a skill with Claude Code (local)",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Headless mode (default - runs in background)
- skill-seekers enhance output/react/
-
- # Interactive mode (opens terminal window)
- skill-seekers enhance output/react/ --interactive-enhancement
-
- # Custom timeout
- skill-seekers enhance output/react/ --timeout 1200
-"""
- )
-
- parser.add_argument(
- 'skill_directory',
- help='Path to skill directory (e.g., output/react/)'
- )
-
- parser.add_argument(
- '--interactive-enhancement',
- action='store_true',
- help='Open terminal window for enhancement (default: headless mode)'
- )
-
- parser.add_argument(
- '--timeout',
- type=int,
- default=600,
- help='Timeout in seconds for headless mode (default: 600 = 10 minutes)'
- )
-
- args = parser.parse_args()
-
- # Run enhancement
- enhancer = LocalSkillEnhancer(args.skill_directory)
- headless = not args.interactive_enhancement # Invert: default is headless
- success = enhancer.run(headless=headless, timeout=args.timeout)
-
- sys.exit(0 if success else 1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/estimate_pages.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/estimate_pages.py
deleted file mode 100644
index c2a23b0..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/estimate_pages.py
+++ /dev/null
@@ -1,288 +0,0 @@
-#!/usr/bin/env python3
-"""
-Page Count Estimator for Skill Seeker
-Quickly estimates how many pages a config will scrape without downloading content
-"""
-
-import sys
-import os
-import requests
-from bs4 import BeautifulSoup
-from urllib.parse import urljoin, urlparse
-import time
-import json
-
-# Add parent directory to path for imports when run as script
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from skill_seekers.cli.constants import (
- DEFAULT_RATE_LIMIT,
- DEFAULT_MAX_DISCOVERY,
- DISCOVERY_THRESHOLD
-)
-
-
-def estimate_pages(config, max_discovery=DEFAULT_MAX_DISCOVERY, timeout=30):
- """
- Estimate total pages that will be scraped
-
- Args:
- config: Configuration dictionary
- max_discovery: Maximum pages to discover (safety limit, use -1 for unlimited)
- timeout: Timeout for HTTP requests in seconds
-
- Returns:
- dict with estimation results
- """
- base_url = config['base_url']
- start_urls = config.get('start_urls', [base_url])
- url_patterns = config.get('url_patterns', {'include': [], 'exclude': []})
- rate_limit = config.get('rate_limit', DEFAULT_RATE_LIMIT)
-
- visited = set()
- pending = list(start_urls)
- discovered = 0
-
- include_patterns = url_patterns.get('include', [])
- exclude_patterns = url_patterns.get('exclude', [])
-
- # Handle unlimited mode
- unlimited = (max_discovery == -1 or max_discovery is None)
-
- print(f"🔍 Estimating pages for: {config['name']}")
- print(f"📍 Base URL: {base_url}")
- print(f"🎯 Start URLs: {len(start_urls)}")
- print(f"⏱️ Rate limit: {rate_limit}s")
-
- if unlimited:
- print(f"🔢 Max discovery: UNLIMITED (will discover all pages)")
- print(f"⚠️ WARNING: This may take a long time!")
- else:
- print(f"🔢 Max discovery: {max_discovery}")
-
- print()
-
- start_time = time.time()
-
- # Loop condition: stop if no more URLs, or if limit reached (when not unlimited)
- while pending and (unlimited or discovered < max_discovery):
- url = pending.pop(0)
-
- # Skip if already visited
- if url in visited:
- continue
-
- visited.add(url)
- discovered += 1
-
- # Progress indicator
- if discovered % 10 == 0:
- elapsed = time.time() - start_time
- rate = discovered / elapsed if elapsed > 0 else 0
- print(f"⏳ Discovered: {discovered} pages ({rate:.1f} pages/sec)", end='\r')
-
- try:
- # HEAD request first to check if page exists (faster)
- head_response = requests.head(url, timeout=timeout, allow_redirects=True)
-
- # Skip non-HTML content
- content_type = head_response.headers.get('Content-Type', '')
- if 'text/html' not in content_type:
- continue
-
- # Now GET the page to find links
- response = requests.get(url, timeout=timeout)
- response.raise_for_status()
-
- soup = BeautifulSoup(response.content, 'html.parser')
-
- # Find all links
- for link in soup.find_all('a', href=True):
- href = link['href']
- full_url = urljoin(url, href)
-
- # Normalize URL
- parsed = urlparse(full_url)
- full_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
-
- # Check if URL is valid
- if not is_valid_url(full_url, base_url, include_patterns, exclude_patterns):
- continue
-
- # Add to pending if not visited
- if full_url not in visited and full_url not in pending:
- pending.append(full_url)
-
- # Rate limiting
- time.sleep(rate_limit)
-
- except requests.RequestException as e:
- # Silently skip errors during estimation
- pass
- except Exception as e:
- # Silently skip other errors
- pass
-
- elapsed = time.time() - start_time
-
- # Results
- results = {
- 'discovered': discovered,
- 'pending': len(pending),
- 'estimated_total': discovered + len(pending),
- 'elapsed_seconds': round(elapsed, 2),
- 'discovery_rate': round(discovered / elapsed if elapsed > 0 else 0, 2),
- 'hit_limit': (not unlimited) and (discovered >= max_discovery),
- 'unlimited': unlimited
- }
-
- return results
-
-
-def is_valid_url(url, base_url, include_patterns, exclude_patterns):
- """Check if URL should be crawled"""
- # Must be same domain
- if not url.startswith(base_url.rstrip('/')):
- return False
-
- # Check exclude patterns first
- if exclude_patterns:
- for pattern in exclude_patterns:
- if pattern in url:
- return False
-
- # Check include patterns (if specified)
- if include_patterns:
- for pattern in include_patterns:
- if pattern in url:
- return True
- return False
-
- # If no include patterns, accept by default
- return True
-
-
-def print_results(results, config):
- """Print estimation results"""
- print()
- print("=" * 70)
- print("📊 ESTIMATION RESULTS")
- print("=" * 70)
- print()
- print(f"Config: {config['name']}")
- print(f"Base URL: {config['base_url']}")
- print()
- print(f"✅ Pages Discovered: {results['discovered']}")
- print(f"⏳ Pages Pending: {results['pending']}")
- print(f"📈 Estimated Total: {results['estimated_total']}")
- print()
- print(f"⏱️ Time Elapsed: {results['elapsed_seconds']}s")
- print(f"⚡ Discovery Rate: {results['discovery_rate']} pages/sec")
-
- if results.get('unlimited', False):
- print()
- print("✅ UNLIMITED MODE - Discovered all reachable pages")
- print(f" Total pages: {results['estimated_total']}")
- elif results['hit_limit']:
- print()
- print("⚠️ Hit discovery limit - actual total may be higher")
- print(" Increase max_discovery parameter for more accurate estimate")
-
- print()
- print("=" * 70)
- print("💡 RECOMMENDATIONS")
- print("=" * 70)
- print()
-
- estimated = results['estimated_total']
- current_max = config.get('max_pages', 100)
-
- if estimated <= current_max:
- print(f"✅ Current max_pages ({current_max}) is sufficient")
- else:
- recommended = min(estimated + 50, DISCOVERY_THRESHOLD) # Add 50 buffer, cap at threshold
- print(f"⚠️ Current max_pages ({current_max}) may be too low")
- print(f"📝 Recommended max_pages: {recommended}")
- print(f" (Estimated {estimated} + 50 buffer)")
-
- # Estimate time for full scrape
- rate_limit = config.get('rate_limit', DEFAULT_RATE_LIMIT)
- estimated_time = (estimated * rate_limit) / 60 # in minutes
-
- print()
- print(f"⏱️ Estimated full scrape time: {estimated_time:.1f} minutes")
- print(f" (Based on rate_limit: {rate_limit}s)")
-
- print()
-
-
-def load_config(config_path):
- """Load configuration from JSON file"""
- try:
- with open(config_path, 'r') as f:
- config = json.load(f)
- return config
- except FileNotFoundError:
- print(f"❌ Error: Config file not found: {config_path}")
- sys.exit(1)
- except json.JSONDecodeError as e:
- print(f"❌ Error: Invalid JSON in config file: {e}")
- sys.exit(1)
-
-
-def main():
- """Main entry point"""
- import argparse
-
- parser = argparse.ArgumentParser(
- description='Estimate page count for Skill Seeker configs',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Estimate pages for a config
- skill-seekers estimate configs/react.json
-
- # Estimate with higher discovery limit
- skill-seekers estimate configs/godot.json --max-discovery 2000
-
- # Quick estimate (stop at 100 pages)
- skill-seekers estimate configs/vue.json --max-discovery 100
- """
- )
-
- parser.add_argument('config', help='Path to config JSON file')
- parser.add_argument('--max-discovery', '-m', type=int, default=DEFAULT_MAX_DISCOVERY,
- help=f'Maximum pages to discover (default: {DEFAULT_MAX_DISCOVERY}, use -1 for unlimited)')
- parser.add_argument('--unlimited', '-u', action='store_true',
- help='Remove discovery limit - discover all pages (same as --max-discovery -1)')
- parser.add_argument('--timeout', '-t', type=int, default=30,
- help='HTTP request timeout in seconds (default: 30)')
-
- args = parser.parse_args()
-
- # Handle unlimited flag
- max_discovery = -1 if args.unlimited else args.max_discovery
-
- # Load config
- config = load_config(args.config)
-
- # Run estimation
- try:
- results = estimate_pages(config, max_discovery, args.timeout)
- print_results(results, config)
-
- # Return exit code based on results
- if results['hit_limit']:
- return 2 # Warning: hit limit
- return 0 # Success
-
- except KeyboardInterrupt:
- print("\n\n⚠️ Estimation interrupted by user")
- return 1
- except Exception as e:
- print(f"\n\n❌ Error during estimation: {e}")
- return 1
-
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/generate_router.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/generate_router.py
deleted file mode 100644
index 0d4ef84..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/generate_router.py
+++ /dev/null
@@ -1,274 +0,0 @@
-#!/usr/bin/env python3
-"""
-Router Skill Generator
-
-Creates a router/hub skill that intelligently directs queries to specialized sub-skills.
-This is used for large documentation sites split into multiple focused skills.
-"""
-
-import json
-import sys
-import argparse
-from pathlib import Path
-from typing import Dict, List, Any, Tuple
-
-
-class RouterGenerator:
- """Generates router skills that direct to specialized sub-skills"""
-
- def __init__(self, config_paths: List[str], router_name: str = None):
- self.config_paths = [Path(p) for p in config_paths]
- self.configs = [self.load_config(p) for p in self.config_paths]
- self.router_name = router_name or self.infer_router_name()
- self.base_config = self.configs[0] # Use first as template
-
- def load_config(self, path: Path) -> Dict[str, Any]:
- """Load a config file"""
- try:
- with open(path, 'r') as f:
- return json.load(f)
- except Exception as e:
- print(f"❌ Error loading {path}: {e}")
- sys.exit(1)
-
- def infer_router_name(self) -> str:
- """Infer router name from sub-skill names"""
- # Find common prefix
- names = [cfg['name'] for cfg in self.configs]
- if not names:
- return "router"
-
- # Get common prefix before first dash
- first_name = names[0]
- if '-' in first_name:
- return first_name.split('-')[0]
- return first_name
-
- def extract_routing_keywords(self) -> Dict[str, List[str]]:
- """Extract keywords for routing to each skill"""
- routing = {}
-
- for config in self.configs:
- name = config['name']
- keywords = []
-
- # Extract from categories
- if 'categories' in config:
- keywords.extend(config['categories'].keys())
-
- # Extract from name (part after dash)
- if '-' in name:
- skill_topic = name.split('-', 1)[1]
- keywords.append(skill_topic)
-
- routing[name] = keywords
-
- return routing
-
- def generate_skill_md(self) -> str:
- """Generate router SKILL.md content"""
- routing_keywords = self.extract_routing_keywords()
-
- skill_md = f"""# {self.router_name.replace('-', ' ').title()} Documentation (Router)
-
-## When to Use This Skill
-
-{self.base_config.get('description', f'Use for {self.router_name} development and programming.')}
-
-This is a router skill that directs your questions to specialized sub-skills for efficient, focused assistance.
-
-## How It Works
-
-This skill analyzes your question and activates the appropriate specialized skill(s):
-
-"""
-
- # List sub-skills
- for config in self.configs:
- name = config['name']
- desc = config.get('description', '')
- # Remove router name prefix from description if present
- if desc.startswith(f"{self.router_name.title()} -"):
- desc = desc.split(' - ', 1)[1]
-
- skill_md += f"### {name}\n{desc}\n\n"
-
- # Routing logic
- skill_md += """## Routing Logic
-
-The router analyzes your question for topic keywords and activates relevant skills:
-
-**Keywords → Skills:**
-"""
-
- for skill_name, keywords in routing_keywords.items():
- keyword_str = ", ".join(keywords)
- skill_md += f"- {keyword_str} → **{skill_name}**\n"
-
- # Quick reference
- skill_md += f"""
-
-## Quick Reference
-
-For quick answers, this router provides basic overview information. For detailed documentation, the specialized skills contain comprehensive references.
-
-### Getting Started
-
-1. Ask your question naturally - mention the topic area
-2. The router will activate the appropriate skill(s)
-3. You'll receive focused, detailed answers from specialized documentation
-
-### Examples
-
-**Question:** "How do I create a 2D sprite?"
-**Activates:** {self.router_name}-2d skill
-
-**Question:** "GDScript function syntax"
-**Activates:** {self.router_name}-scripting skill
-
-**Question:** "Physics collision handling in 3D"
-**Activates:** {self.router_name}-3d + {self.router_name}-physics skills
-
-### All Available Skills
-
-"""
-
- # List all skills
- for config in self.configs:
- skill_md += f"- **{config['name']}**\n"
-
- skill_md += f"""
-
-## Need Help?
-
-Simply ask your question and mention the topic. The router will find the right specialized skill for you!
-
----
-
-*This is a router skill. For complete documentation, see the specialized skills listed above.*
-"""
-
- return skill_md
-
- def create_router_config(self) -> Dict[str, Any]:
- """Create router configuration"""
- routing_keywords = self.extract_routing_keywords()
-
- router_config = {
- "name": self.router_name,
- "description": self.base_config.get('description', f'{self.router_name.title()} documentation router'),
- "base_url": self.base_config['base_url'],
- "selectors": self.base_config.get('selectors', {}),
- "url_patterns": self.base_config.get('url_patterns', {}),
- "rate_limit": self.base_config.get('rate_limit', 0.5),
- "max_pages": 500, # Router only scrapes overview pages
- "_router": True,
- "_sub_skills": [cfg['name'] for cfg in self.configs],
- "_routing_keywords": routing_keywords
- }
-
- return router_config
-
- def generate(self, output_dir: Path = None) -> Tuple[Path, Path]:
- """Generate router skill and config"""
- if output_dir is None:
- output_dir = self.config_paths[0].parent
-
- output_dir = Path(output_dir)
-
- # Generate SKILL.md
- skill_md = self.generate_skill_md()
- skill_path = output_dir.parent / f"output/{self.router_name}/SKILL.md"
- skill_path.parent.mkdir(parents=True, exist_ok=True)
-
- with open(skill_path, 'w') as f:
- f.write(skill_md)
-
- # Generate config
- router_config = self.create_router_config()
- config_path = output_dir / f"{self.router_name}.json"
-
- with open(config_path, 'w') as f:
- json.dump(router_config, f, indent=2)
-
- return config_path, skill_path
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Generate router/hub skill for split documentation",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Generate router from multiple configs
- python3 generate_router.py configs/godot-2d.json configs/godot-3d.json configs/godot-scripting.json
-
- # Use glob pattern
- python3 generate_router.py configs/godot-*.json
-
- # Custom router name
- python3 generate_router.py configs/godot-*.json --name godot-hub
-
- # Custom output directory
- python3 generate_router.py configs/godot-*.json --output-dir configs/routers/
- """
- )
-
- parser.add_argument(
- 'configs',
- nargs='+',
- help='Sub-skill config files'
- )
-
- parser.add_argument(
- '--name',
- help='Router skill name (default: inferred from sub-skills)'
- )
-
- parser.add_argument(
- '--output-dir',
- help='Output directory (default: same as input configs)'
- )
-
- args = parser.parse_args()
-
- # Filter out router configs (avoid recursion)
- config_files = []
- for path_str in args.configs:
- path = Path(path_str)
- if path.exists() and not path.stem.endswith('-router'):
- config_files.append(path_str)
-
- if not config_files:
- print("❌ Error: No valid config files provided")
- sys.exit(1)
-
- print(f"\n{'='*60}")
- print("ROUTER SKILL GENERATOR")
- print(f"{'='*60}")
- print(f"Sub-skills: {len(config_files)}")
- for cfg in config_files:
- print(f" - {Path(cfg).stem}")
- print("")
-
- # Generate router
- generator = RouterGenerator(config_files, args.name)
- config_path, skill_path = generator.generate(args.output_dir)
-
- print(f"✅ Router config created: {config_path}")
- print(f"✅ Router SKILL.md created: {skill_path}")
- print("")
- print(f"{'='*60}")
- print("NEXT STEPS")
- print(f"{'='*60}")
- print(f"1. Review router SKILL.md: {skill_path}")
- print(f"2. Optionally scrape router (for overview pages):")
- print(f" skill-seekers scrape --config {config_path}")
- print("3. Package router skill:")
- print(f" skill-seekers package output/{generator.router_name}/")
- print("4. Upload router + all sub-skills to Claude")
- print("")
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/github_scraper.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/github_scraper.py
deleted file mode 100644
index 861f6c6..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/github_scraper.py
+++ /dev/null
@@ -1,900 +0,0 @@
-#!/usr/bin/env python3
-"""
-GitHub Repository to Claude Skill Converter (Tasks C1.1-C1.12)
-
-Converts GitHub repositories into Claude AI skills by extracting:
-- README and documentation
-- Code structure and signatures
-- GitHub Issues, Changelog, and Releases
-- Usage examples from tests
-
-Usage:
- skill-seekers github --repo facebook/react
- skill-seekers github --config configs/react_github.json
- skill-seekers github --repo owner/repo --token $GITHUB_TOKEN
-"""
-
-import os
-import sys
-import json
-import re
-import argparse
-import logging
-from pathlib import Path
-from typing import Dict, List, Optional, Any
-from datetime import datetime
-
-try:
- from github import Github, GithubException, Repository
- from github.GithubException import RateLimitExceededException
-except ImportError:
- print("Error: PyGithub not installed. Run: pip install PyGithub")
- sys.exit(1)
-
-# Configure logging FIRST (before using logger)
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-# Import code analyzer for deep code analysis
-try:
- from .code_analyzer import CodeAnalyzer
- CODE_ANALYZER_AVAILABLE = True
-except ImportError:
- CODE_ANALYZER_AVAILABLE = False
- logger.warning("Code analyzer not available - deep analysis disabled")
-
-# Directories to exclude from local repository analysis
-EXCLUDED_DIRS = {
- 'venv', 'env', '.venv', '.env', # Virtual environments
- 'node_modules', '__pycache__', '.pytest_cache', # Dependencies and caches
- '.git', '.svn', '.hg', # Version control
- 'build', 'dist', '*.egg-info', # Build artifacts
- 'htmlcov', '.coverage', # Coverage reports
- '.tox', '.nox', # Testing environments
- '.mypy_cache', '.ruff_cache', # Linter caches
-}
-
-
-class GitHubScraper:
- """
- GitHub Repository Scraper (C1.1-C1.9)
-
- Extracts repository information for skill generation:
- - Repository structure
- - README files
- - Code comments and docstrings
- - Programming language detection
- - Function/class signatures
- - Test examples
- - GitHub Issues
- - CHANGELOG
- - Releases
- """
-
- def __init__(self, config: Dict[str, Any], local_repo_path: Optional[str] = None):
- """Initialize GitHub scraper with configuration."""
- self.config = config
- self.repo_name = config['repo']
- self.name = config.get('name', self.repo_name.split('/')[-1])
- self.description = config.get('description', f'Skill for {self.repo_name}')
-
- # Local repository path (optional - enables unlimited analysis)
- self.local_repo_path = local_repo_path or config.get('local_repo_path')
- if self.local_repo_path:
- self.local_repo_path = os.path.expanduser(self.local_repo_path)
- logger.info(f"Local repository mode enabled: {self.local_repo_path}")
-
- # Configure directory exclusions (smart defaults + optional customization)
- self.excluded_dirs = set(EXCLUDED_DIRS) # Start with smart defaults
-
- # Option 1: Replace mode - Use only specified exclusions
- if 'exclude_dirs' in config:
- self.excluded_dirs = set(config['exclude_dirs'])
- logger.warning(
- f"Using custom directory exclusions ({len(self.excluded_dirs)} dirs) - "
- "defaults overridden"
- )
- logger.debug(f"Custom exclusions: {sorted(self.excluded_dirs)}")
-
- # Option 2: Extend mode - Add to default exclusions
- elif 'exclude_dirs_additional' in config:
- additional = set(config['exclude_dirs_additional'])
- self.excluded_dirs = self.excluded_dirs.union(additional)
- logger.info(
- f"Added {len(additional)} custom directory exclusions "
- f"(total: {len(self.excluded_dirs)})"
- )
- logger.debug(f"Additional exclusions: {sorted(additional)}")
-
- # GitHub client setup (C1.1)
- token = self._get_token()
- self.github = Github(token) if token else Github()
- self.repo: Optional[Repository.Repository] = None
-
- # Options
- self.include_issues = config.get('include_issues', True)
- self.max_issues = config.get('max_issues', 100)
- self.include_changelog = config.get('include_changelog', True)
- self.include_releases = config.get('include_releases', True)
- self.include_code = config.get('include_code', False)
- self.code_analysis_depth = config.get('code_analysis_depth', 'surface') # 'surface', 'deep', 'full'
- self.file_patterns = config.get('file_patterns', [])
-
- # Initialize code analyzer if deep analysis requested
- self.code_analyzer = None
- if self.code_analysis_depth != 'surface' and CODE_ANALYZER_AVAILABLE:
- self.code_analyzer = CodeAnalyzer(depth=self.code_analysis_depth)
- logger.info(f"Code analysis depth: {self.code_analysis_depth}")
-
- # Output paths
- self.skill_dir = f"output/{self.name}"
- self.data_file = f"output/{self.name}_github_data.json"
-
- # Extracted data storage
- self.extracted_data = {
- 'repo_info': {},
- 'readme': '',
- 'file_tree': [],
- 'languages': {},
- 'signatures': [],
- 'test_examples': [],
- 'issues': [],
- 'changelog': '',
- 'releases': []
- }
-
- def _get_token(self) -> Optional[str]:
- """
- Get GitHub token from env var or config (both options supported).
- Priority: GITHUB_TOKEN env var > config file > None
- """
- # Try environment variable first (recommended)
- token = os.getenv('GITHUB_TOKEN')
- if token:
- logger.info("Using GitHub token from GITHUB_TOKEN environment variable")
- return token
-
- # Fall back to config file
- token = self.config.get('github_token')
- if token:
- logger.warning("Using GitHub token from config file (less secure)")
- return token
-
- logger.warning("No GitHub token provided - using unauthenticated access (lower rate limits)")
- return None
-
- def scrape(self) -> Dict[str, Any]:
- """
- Main scraping entry point.
- Executes all C1 tasks in sequence.
- """
- try:
- logger.info(f"Starting GitHub scrape for: {self.repo_name}")
-
- # C1.1: Fetch repository
- self._fetch_repository()
-
- # C1.2: Extract README
- self._extract_readme()
-
- # C1.3-C1.6: Extract code structure
- self._extract_code_structure()
-
- # C1.7: Extract Issues
- if self.include_issues:
- self._extract_issues()
-
- # C1.8: Extract CHANGELOG
- if self.include_changelog:
- self._extract_changelog()
-
- # C1.9: Extract Releases
- if self.include_releases:
- self._extract_releases()
-
- # Save extracted data
- self._save_data()
-
- logger.info(f"✅ Scraping complete! Data saved to: {self.data_file}")
- return self.extracted_data
-
- except RateLimitExceededException:
- logger.error("GitHub API rate limit exceeded. Please wait or use authentication token.")
- raise
- except GithubException as e:
- logger.error(f"GitHub API error: {e}")
- raise
- except Exception as e:
- logger.error(f"Unexpected error during scraping: {e}")
- raise
-
- def _fetch_repository(self):
- """C1.1: Fetch repository structure using GitHub API."""
- logger.info(f"Fetching repository: {self.repo_name}")
-
- try:
- self.repo = self.github.get_repo(self.repo_name)
-
- # Extract basic repo info
- self.extracted_data['repo_info'] = {
- 'name': self.repo.name,
- 'full_name': self.repo.full_name,
- 'description': self.repo.description,
- 'url': self.repo.html_url,
- 'homepage': self.repo.homepage,
- 'stars': self.repo.stargazers_count,
- 'forks': self.repo.forks_count,
- 'open_issues': self.repo.open_issues_count,
- 'default_branch': self.repo.default_branch,
- 'created_at': self.repo.created_at.isoformat() if self.repo.created_at else None,
- 'updated_at': self.repo.updated_at.isoformat() if self.repo.updated_at else None,
- 'language': self.repo.language,
- 'license': self.repo.license.name if self.repo.license else None,
- 'topics': self.repo.get_topics()
- }
-
- logger.info(f"Repository fetched: {self.repo.full_name} ({self.repo.stargazers_count} stars)")
-
- except GithubException as e:
- if e.status == 404:
- raise ValueError(f"Repository not found: {self.repo_name}")
- raise
-
- def _extract_readme(self):
- """C1.2: Extract README.md files."""
- logger.info("Extracting README...")
-
- # Try common README locations
- readme_files = ['README.md', 'README.rst', 'README.txt', 'README',
- 'docs/README.md', '.github/README.md']
-
- for readme_path in readme_files:
- try:
- content = self.repo.get_contents(readme_path)
- if content:
- self.extracted_data['readme'] = content.decoded_content.decode('utf-8')
- logger.info(f"README found: {readme_path}")
- return
- except GithubException:
- continue
-
- logger.warning("No README found in repository")
-
- def _extract_code_structure(self):
- """
- C1.3-C1.6: Extract code structure, languages, signatures, and test examples.
- Surface layer only - no full implementation code.
- """
- logger.info("Extracting code structure...")
-
- # C1.4: Get language breakdown
- self._extract_languages()
-
- # Get file tree
- self._extract_file_tree()
-
- # Extract signatures and test examples
- if self.include_code:
- self._extract_signatures_and_tests()
-
- def _extract_languages(self):
- """C1.4: Detect programming languages in repository."""
- logger.info("Detecting programming languages...")
-
- try:
- languages = self.repo.get_languages()
- total_bytes = sum(languages.values())
-
- self.extracted_data['languages'] = {
- lang: {
- 'bytes': bytes_count,
- 'percentage': round((bytes_count / total_bytes) * 100, 2) if total_bytes > 0 else 0
- }
- for lang, bytes_count in languages.items()
- }
-
- logger.info(f"Languages detected: {', '.join(languages.keys())}")
-
- except GithubException as e:
- logger.warning(f"Could not fetch languages: {e}")
-
- def should_exclude_dir(self, dir_name: str) -> bool:
- """Check if directory should be excluded from analysis."""
- return dir_name in self.excluded_dirs or dir_name.startswith('.')
-
- def _extract_file_tree(self):
- """Extract repository file tree structure (dual-mode: GitHub API or local filesystem)."""
- logger.info("Building file tree...")
-
- if self.local_repo_path:
- # Local filesystem mode - unlimited files
- self._extract_file_tree_local()
- else:
- # GitHub API mode - limited by API rate limits
- self._extract_file_tree_github()
-
- def _extract_file_tree_local(self):
- """Extract file tree from local filesystem (unlimited files)."""
- if not os.path.exists(self.local_repo_path):
- logger.error(f"Local repository path not found: {self.local_repo_path}")
- return
-
- file_tree = []
- for root, dirs, files in os.walk(self.local_repo_path):
- # Exclude directories in-place to prevent os.walk from descending into them
- dirs[:] = [d for d in dirs if not self.should_exclude_dir(d)]
-
- # Calculate relative path from repo root
- rel_root = os.path.relpath(root, self.local_repo_path)
- if rel_root == '.':
- rel_root = ''
-
- # Add directories
- for dir_name in dirs:
- dir_path = os.path.join(rel_root, dir_name) if rel_root else dir_name
- file_tree.append({
- 'path': dir_path,
- 'type': 'dir',
- 'size': None
- })
-
- # Add files
- for file_name in files:
- file_path = os.path.join(rel_root, file_name) if rel_root else file_name
- full_path = os.path.join(root, file_name)
- try:
- file_size = os.path.getsize(full_path)
- except OSError:
- file_size = None
-
- file_tree.append({
- 'path': file_path,
- 'type': 'file',
- 'size': file_size
- })
-
- self.extracted_data['file_tree'] = file_tree
- logger.info(f"File tree built (local mode): {len(file_tree)} items")
-
- def _extract_file_tree_github(self):
- """Extract file tree from GitHub API (rate-limited)."""
- try:
- contents = self.repo.get_contents("")
- file_tree = []
-
- while contents:
- file_content = contents.pop(0)
-
- file_info = {
- 'path': file_content.path,
- 'type': file_content.type,
- 'size': file_content.size if file_content.type == 'file' else None
- }
- file_tree.append(file_info)
-
- if file_content.type == "dir":
- contents.extend(self.repo.get_contents(file_content.path))
-
- self.extracted_data['file_tree'] = file_tree
- logger.info(f"File tree built (GitHub API mode): {len(file_tree)} items")
-
- except GithubException as e:
- logger.warning(f"Could not build file tree: {e}")
-
- def _extract_signatures_and_tests(self):
- """
- C1.3, C1.5, C1.6: Extract signatures, docstrings, and test examples.
-
- Extraction depth depends on code_analysis_depth setting:
- - surface: File tree only (minimal)
- - deep: Parse files for signatures, parameters, types
- - full: Complete AST analysis (future enhancement)
- """
- if self.code_analysis_depth == 'surface':
- logger.info("Code extraction: Surface level (file tree only)")
- return
-
- if not self.code_analyzer:
- logger.warning("Code analyzer not available - skipping deep analysis")
- return
-
- logger.info(f"Extracting code signatures ({self.code_analysis_depth} analysis)...")
-
- # Get primary language for the repository
- languages = self.extracted_data.get('languages', {})
- if not languages:
- logger.warning("No languages detected - skipping code analysis")
- return
-
- # Determine primary language
- primary_language = max(languages.items(), key=lambda x: x[1]['bytes'])[0]
- logger.info(f"Primary language: {primary_language}")
-
- # Determine file extensions to analyze
- extension_map = {
- 'Python': ['.py'],
- 'JavaScript': ['.js', '.jsx'],
- 'TypeScript': ['.ts', '.tsx'],
- 'C': ['.c', '.h'],
- 'C++': ['.cpp', '.hpp', '.cc', '.hh', '.cxx']
- }
-
- extensions = extension_map.get(primary_language, [])
- if not extensions:
- logger.warning(f"No file extensions mapped for {primary_language}")
- return
-
- # Analyze files matching patterns and extensions
- analyzed_files = []
- file_tree = self.extracted_data.get('file_tree', [])
-
- for file_info in file_tree:
- file_path = file_info['path']
-
- # Check if file matches extension
- if not any(file_path.endswith(ext) for ext in extensions):
- continue
-
- # Check if file matches patterns (if specified)
- if self.file_patterns:
- import fnmatch
- if not any(fnmatch.fnmatch(file_path, pattern) for pattern in self.file_patterns):
- continue
-
- # Analyze this file
- try:
- # Read file content based on mode
- if self.local_repo_path:
- # Local mode - read from filesystem
- full_path = os.path.join(self.local_repo_path, file_path)
- with open(full_path, 'r', encoding='utf-8') as f:
- content = f.read()
- else:
- # GitHub API mode - fetch from API
- file_content = self.repo.get_contents(file_path)
- content = file_content.decoded_content.decode('utf-8')
-
- analysis_result = self.code_analyzer.analyze_file(
- file_path,
- content,
- primary_language
- )
-
- if analysis_result and (analysis_result.get('classes') or analysis_result.get('functions')):
- analyzed_files.append({
- 'file': file_path,
- 'language': primary_language,
- **analysis_result
- })
-
- logger.debug(f"Analyzed {file_path}: "
- f"{len(analysis_result.get('classes', []))} classes, "
- f"{len(analysis_result.get('functions', []))} functions")
-
- except Exception as e:
- logger.debug(f"Could not analyze {file_path}: {e}")
- continue
-
- # Limit number of files analyzed to avoid rate limits (GitHub API mode only)
- if not self.local_repo_path and len(analyzed_files) >= 50:
- logger.info(f"Reached analysis limit (50 files, GitHub API mode)")
- break
-
- self.extracted_data['code_analysis'] = {
- 'depth': self.code_analysis_depth,
- 'language': primary_language,
- 'files_analyzed': len(analyzed_files),
- 'files': analyzed_files
- }
-
- # Calculate totals
- total_classes = sum(len(f.get('classes', [])) for f in analyzed_files)
- total_functions = sum(len(f.get('functions', [])) for f in analyzed_files)
-
- logger.info(f"Code analysis complete: {len(analyzed_files)} files, "
- f"{total_classes} classes, {total_functions} functions")
-
- def _extract_issues(self):
- """C1.7: Extract GitHub Issues (open/closed, labels, milestones)."""
- logger.info(f"Extracting GitHub Issues (max {self.max_issues})...")
-
- try:
- # Fetch recent issues (open + closed)
- issues = self.repo.get_issues(state='all', sort='updated', direction='desc')
-
- issue_list = []
- for issue in issues[:self.max_issues]:
- # Skip pull requests (they appear in issues)
- if issue.pull_request:
- continue
-
- issue_data = {
- 'number': issue.number,
- 'title': issue.title,
- 'state': issue.state,
- 'labels': [label.name for label in issue.labels],
- 'milestone': issue.milestone.title if issue.milestone else None,
- 'created_at': issue.created_at.isoformat() if issue.created_at else None,
- 'updated_at': issue.updated_at.isoformat() if issue.updated_at else None,
- 'closed_at': issue.closed_at.isoformat() if issue.closed_at else None,
- 'url': issue.html_url,
- 'body': issue.body[:500] if issue.body else None # First 500 chars
- }
- issue_list.append(issue_data)
-
- self.extracted_data['issues'] = issue_list
- logger.info(f"Extracted {len(issue_list)} issues")
-
- except GithubException as e:
- logger.warning(f"Could not fetch issues: {e}")
-
- def _extract_changelog(self):
- """C1.8: Extract CHANGELOG.md and release notes."""
- logger.info("Extracting CHANGELOG...")
-
- # Try common changelog locations
- changelog_files = ['CHANGELOG.md', 'CHANGES.md', 'HISTORY.md',
- 'CHANGELOG.rst', 'CHANGELOG.txt', 'CHANGELOG',
- 'docs/CHANGELOG.md', '.github/CHANGELOG.md']
-
- for changelog_path in changelog_files:
- try:
- content = self.repo.get_contents(changelog_path)
- if content:
- self.extracted_data['changelog'] = content.decoded_content.decode('utf-8')
- logger.info(f"CHANGELOG found: {changelog_path}")
- return
- except GithubException:
- continue
-
- logger.warning("No CHANGELOG found in repository")
-
- def _extract_releases(self):
- """C1.9: Extract GitHub Releases with version history."""
- logger.info("Extracting GitHub Releases...")
-
- try:
- releases = self.repo.get_releases()
-
- release_list = []
- for release in releases:
- release_data = {
- 'tag_name': release.tag_name,
- 'name': release.title,
- 'body': release.body,
- 'draft': release.draft,
- 'prerelease': release.prerelease,
- 'created_at': release.created_at.isoformat() if release.created_at else None,
- 'published_at': release.published_at.isoformat() if release.published_at else None,
- 'url': release.html_url,
- 'tarball_url': release.tarball_url,
- 'zipball_url': release.zipball_url
- }
- release_list.append(release_data)
-
- self.extracted_data['releases'] = release_list
- logger.info(f"Extracted {len(release_list)} releases")
-
- except GithubException as e:
- logger.warning(f"Could not fetch releases: {e}")
-
- def _save_data(self):
- """Save extracted data to JSON file."""
- os.makedirs('output', exist_ok=True)
-
- with open(self.data_file, 'w', encoding='utf-8') as f:
- json.dump(self.extracted_data, f, indent=2, ensure_ascii=False)
-
- logger.info(f"Data saved to: {self.data_file}")
-
-
-class GitHubToSkillConverter:
- """
- Convert extracted GitHub data to Claude skill format (C1.10).
- """
-
- def __init__(self, config: Dict[str, Any]):
- """Initialize converter with configuration."""
- self.config = config
- self.name = config.get('name', config['repo'].split('/')[-1])
- self.description = config.get('description', f'Skill for {config["repo"]}')
-
- # Paths
- self.data_file = f"output/{self.name}_github_data.json"
- self.skill_dir = f"output/{self.name}"
-
- # Load extracted data
- self.data = self._load_data()
-
- def _load_data(self) -> Dict[str, Any]:
- """Load extracted GitHub data from JSON."""
- if not os.path.exists(self.data_file):
- raise FileNotFoundError(f"Data file not found: {self.data_file}")
-
- with open(self.data_file, 'r', encoding='utf-8') as f:
- return json.load(f)
-
- def build_skill(self):
- """Build complete skill structure."""
- logger.info(f"Building skill for: {self.name}")
-
- # Create directories
- os.makedirs(self.skill_dir, exist_ok=True)
- os.makedirs(f"{self.skill_dir}/references", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/scripts", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/assets", exist_ok=True)
-
- # Generate SKILL.md
- self._generate_skill_md()
-
- # Generate reference files
- self._generate_references()
-
- logger.info(f"✅ Skill built successfully: {self.skill_dir}/")
-
- def _generate_skill_md(self):
- """Generate main SKILL.md file."""
- repo_info = self.data.get('repo_info', {})
-
- # Generate skill name (lowercase, hyphens only, max 64 chars)
- skill_name = self.name.lower().replace('_', '-').replace(' ', '-')[:64]
-
- # Truncate description to 1024 chars if needed
- desc = self.description[:1024] if len(self.description) > 1024 else self.description
-
- skill_content = f"""---
-name: {skill_name}
-description: {desc}
----
-
-# {repo_info.get('name', self.name)}
-
-{self.description}
-
-## Description
-
-{repo_info.get('description', 'GitHub repository skill')}
-
-**Repository:** [{repo_info.get('full_name', 'N/A')}]({repo_info.get('url', '#')})
-**Language:** {repo_info.get('language', 'N/A')}
-**Stars:** {repo_info.get('stars', 0):,}
-**License:** {repo_info.get('license', 'N/A')}
-
-## When to Use This Skill
-
-Use this skill when you need to:
-- Understand how to use {self.name}
-- Look up API documentation
-- Find usage examples
-- Check for known issues or recent changes
-- Review release history
-
-## Quick Reference
-
-### Repository Info
-- **Homepage:** {repo_info.get('homepage', 'N/A')}
-- **Topics:** {', '.join(repo_info.get('topics', []))}
-- **Open Issues:** {repo_info.get('open_issues', 0)}
-- **Last Updated:** {repo_info.get('updated_at', 'N/A')[:10]}
-
-### Languages
-{self._format_languages()}
-
-### Recent Releases
-{self._format_recent_releases()}
-
-## Available References
-
-- `references/README.md` - Complete README documentation
-- `references/CHANGELOG.md` - Version history and changes
-- `references/issues.md` - Recent GitHub issues
-- `references/releases.md` - Release notes
-- `references/file_structure.md` - Repository structure
-
-## Usage
-
-See README.md for complete usage instructions and examples.
-
----
-
-**Generated by Skill Seeker** | GitHub Repository Scraper
-"""
-
- skill_path = f"{self.skill_dir}/SKILL.md"
- with open(skill_path, 'w', encoding='utf-8') as f:
- f.write(skill_content)
-
- logger.info(f"Generated: {skill_path}")
-
- def _format_languages(self) -> str:
- """Format language breakdown."""
- languages = self.data.get('languages', {})
- if not languages:
- return "No language data available"
-
- lines = []
- for lang, info in sorted(languages.items(), key=lambda x: x[1]['bytes'], reverse=True):
- lines.append(f"- **{lang}:** {info['percentage']:.1f}%")
-
- return '\n'.join(lines)
-
- def _format_recent_releases(self) -> str:
- """Format recent releases (top 3)."""
- releases = self.data.get('releases', [])
- if not releases:
- return "No releases available"
-
- lines = []
- for release in releases[:3]:
- lines.append(f"- **{release['tag_name']}** ({release['published_at'][:10]}): {release['name']}")
-
- return '\n'.join(lines)
-
- def _generate_references(self):
- """Generate all reference files."""
- # README
- if self.data.get('readme'):
- readme_path = f"{self.skill_dir}/references/README.md"
- with open(readme_path, 'w', encoding='utf-8') as f:
- f.write(self.data['readme'])
- logger.info(f"Generated: {readme_path}")
-
- # CHANGELOG
- if self.data.get('changelog'):
- changelog_path = f"{self.skill_dir}/references/CHANGELOG.md"
- with open(changelog_path, 'w', encoding='utf-8') as f:
- f.write(self.data['changelog'])
- logger.info(f"Generated: {changelog_path}")
-
- # Issues
- if self.data.get('issues'):
- self._generate_issues_reference()
-
- # Releases
- if self.data.get('releases'):
- self._generate_releases_reference()
-
- # File structure
- if self.data.get('file_tree'):
- self._generate_file_structure_reference()
-
- def _generate_issues_reference(self):
- """Generate issues.md reference file."""
- issues = self.data['issues']
-
- content = f"# GitHub Issues\n\nRecent issues from the repository ({len(issues)} total).\n\n"
-
- # Group by state
- open_issues = [i for i in issues if i['state'] == 'open']
- closed_issues = [i for i in issues if i['state'] == 'closed']
-
- content += f"## Open Issues ({len(open_issues)})\n\n"
- for issue in open_issues[:20]:
- labels = ', '.join(issue['labels']) if issue['labels'] else 'No labels'
- content += f"### #{issue['number']}: {issue['title']}\n"
- content += f"**Labels:** {labels} | **Created:** {issue['created_at'][:10]}\n"
- content += f"[View on GitHub]({issue['url']})\n\n"
-
- content += f"\n## Recently Closed Issues ({len(closed_issues)})\n\n"
- for issue in closed_issues[:10]:
- labels = ', '.join(issue['labels']) if issue['labels'] else 'No labels'
- content += f"### #{issue['number']}: {issue['title']}\n"
- content += f"**Labels:** {labels} | **Closed:** {issue['closed_at'][:10]}\n"
- content += f"[View on GitHub]({issue['url']})\n\n"
-
- issues_path = f"{self.skill_dir}/references/issues.md"
- with open(issues_path, 'w', encoding='utf-8') as f:
- f.write(content)
- logger.info(f"Generated: {issues_path}")
-
- def _generate_releases_reference(self):
- """Generate releases.md reference file."""
- releases = self.data['releases']
-
- content = f"# Releases\n\nVersion history for this repository ({len(releases)} releases).\n\n"
-
- for release in releases:
- content += f"## {release['tag_name']}: {release['name']}\n"
- content += f"**Published:** {release['published_at'][:10]}\n"
- if release['prerelease']:
- content += f"**Pre-release**\n"
- content += f"\n{release['body']}\n\n"
- content += f"[View on GitHub]({release['url']})\n\n---\n\n"
-
- releases_path = f"{self.skill_dir}/references/releases.md"
- with open(releases_path, 'w', encoding='utf-8') as f:
- f.write(content)
- logger.info(f"Generated: {releases_path}")
-
- def _generate_file_structure_reference(self):
- """Generate file_structure.md reference file."""
- file_tree = self.data['file_tree']
-
- content = f"# Repository File Structure\n\n"
- content += f"Total items: {len(file_tree)}\n\n"
- content += "```\n"
-
- # Build tree structure
- for item in file_tree:
- indent = " " * item['path'].count('/')
- icon = "📁" if item['type'] == 'dir' else "📄"
- content += f"{indent}{icon} {os.path.basename(item['path'])}\n"
-
- content += "```\n"
-
- structure_path = f"{self.skill_dir}/references/file_structure.md"
- with open(structure_path, 'w', encoding='utf-8') as f:
- f.write(content)
- logger.info(f"Generated: {structure_path}")
-
-
-def main():
- """C1.10: CLI tool entry point."""
- parser = argparse.ArgumentParser(
- description='GitHub Repository to Claude Skill Converter',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- skill-seekers github --repo facebook/react
- skill-seekers github --config configs/react_github.json
- skill-seekers github --repo owner/repo --token $GITHUB_TOKEN
- """
- )
-
- parser.add_argument('--repo', help='GitHub repository (owner/repo)')
- parser.add_argument('--config', help='Path to config JSON file')
- parser.add_argument('--token', help='GitHub personal access token')
- parser.add_argument('--name', help='Skill name (default: repo name)')
- parser.add_argument('--description', help='Skill description')
- parser.add_argument('--no-issues', action='store_true', help='Skip GitHub issues')
- parser.add_argument('--no-changelog', action='store_true', help='Skip CHANGELOG')
- parser.add_argument('--no-releases', action='store_true', help='Skip releases')
- parser.add_argument('--max-issues', type=int, default=100, help='Max issues to fetch')
- parser.add_argument('--scrape-only', action='store_true', help='Only scrape, don\'t build skill')
-
- args = parser.parse_args()
-
- # Build config from args or file
- if args.config:
- with open(args.config, 'r') as f:
- config = json.load(f)
- elif args.repo:
- config = {
- 'repo': args.repo,
- 'name': args.name or args.repo.split('/')[-1],
- 'description': args.description or f'GitHub repository skill for {args.repo}',
- 'github_token': args.token,
- 'include_issues': not args.no_issues,
- 'include_changelog': not args.no_changelog,
- 'include_releases': not args.no_releases,
- 'max_issues': args.max_issues
- }
- else:
- parser.error('Either --repo or --config is required')
-
- try:
- # Phase 1: Scrape GitHub repository
- scraper = GitHubScraper(config)
- scraper.scrape()
-
- if args.scrape_only:
- logger.info("Scrape complete (--scrape-only mode)")
- return
-
- # Phase 2: Build skill
- converter = GitHubToSkillConverter(config)
- converter.build_skill()
-
- logger.info(f"\n✅ Success! Skill created at: output/{config.get('name', config['repo'].split('/')[-1])}/")
- logger.info(f"Next step: skill-seekers-package output/{config.get('name', config['repo'].split('/')[-1])}/")
-
- except Exception as e:
- logger.error(f"Error: {e}")
- sys.exit(1)
-
-
-if __name__ == '__main__':
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_detector.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_detector.py
deleted file mode 100644
index 688fdb7..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_detector.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# ABOUTME: Detects and validates llms.txt file availability at documentation URLs
-# ABOUTME: Supports llms-full.txt, llms.txt, and llms-small.txt variants
-
-import requests
-from typing import Optional, Dict, List
-from urllib.parse import urlparse
-
-class LlmsTxtDetector:
- """Detect llms.txt files at documentation URLs"""
-
- VARIANTS = [
- ('llms-full.txt', 'full'),
- ('llms.txt', 'standard'),
- ('llms-small.txt', 'small')
- ]
-
- def __init__(self, base_url: str):
- self.base_url = base_url.rstrip('/')
-
- def detect(self) -> Optional[Dict[str, str]]:
- """
- Detect available llms.txt variant.
-
- Returns:
- Dict with 'url' and 'variant' keys, or None if not found
- """
- parsed = urlparse(self.base_url)
- root_url = f"{parsed.scheme}://{parsed.netloc}"
-
- for filename, variant in self.VARIANTS:
- url = f"{root_url}/{filename}"
-
- if self._check_url_exists(url):
- return {'url': url, 'variant': variant}
-
- return None
-
- def detect_all(self) -> List[Dict[str, str]]:
- """
- Detect all available llms.txt variants.
-
- Returns:
- List of dicts with 'url' and 'variant' keys for each found variant
- """
- found_variants = []
-
- for filename, variant in self.VARIANTS:
- parsed = urlparse(self.base_url)
- root_url = f"{parsed.scheme}://{parsed.netloc}"
- url = f"{root_url}/{filename}"
-
- if self._check_url_exists(url):
- found_variants.append({
- 'url': url,
- 'variant': variant
- })
-
- return found_variants
-
- def _check_url_exists(self, url: str) -> bool:
- """Check if URL returns 200 status"""
- try:
- response = requests.head(url, timeout=5, allow_redirects=True)
- return response.status_code == 200
- except requests.RequestException:
- return False
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_downloader.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_downloader.py
deleted file mode 100644
index 1049f86..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_downloader.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""ABOUTME: Downloads llms.txt files from documentation URLs with retry logic"""
-"""ABOUTME: Validates markdown content and handles timeouts with exponential backoff"""
-
-import requests
-import time
-from typing import Optional
-
-class LlmsTxtDownloader:
- """Download llms.txt content from URLs with retry logic"""
-
- def __init__(self, url: str, timeout: int = 30, max_retries: int = 3):
- self.url = url
- self.timeout = timeout
- self.max_retries = max_retries
-
- def get_proper_filename(self) -> str:
- """
- Extract filename from URL and convert .txt to .md
-
- Returns:
- Proper filename with .md extension
-
- Examples:
- https://hono.dev/llms-full.txt -> llms-full.md
- https://hono.dev/llms.txt -> llms.md
- https://hono.dev/llms-small.txt -> llms-small.md
- """
- # Extract filename from URL
- from urllib.parse import urlparse
- parsed = urlparse(self.url)
- filename = parsed.path.split('/')[-1]
-
- # Replace .txt with .md
- if filename.endswith('.txt'):
- filename = filename[:-4] + '.md'
-
- return filename
-
- def _is_markdown(self, content: str) -> bool:
- """
- Check if content looks like markdown.
-
- Returns:
- True if content contains markdown patterns
- """
- markdown_patterns = ['# ', '## ', '```', '- ', '* ', '`']
- return any(pattern in content for pattern in markdown_patterns)
-
- def download(self) -> Optional[str]:
- """
- Download llms.txt content with retry logic.
-
- Returns:
- String content or None if download fails
- """
- headers = {
- 'User-Agent': 'Skill-Seekers-llms.txt-Reader/1.0'
- }
-
- for attempt in range(self.max_retries):
- try:
- response = requests.get(
- self.url,
- headers=headers,
- timeout=self.timeout
- )
- response.raise_for_status()
-
- content = response.text
-
- # Validate content is not empty
- if len(content) < 100:
- print(f"⚠️ Content too short ({len(content)} chars), rejecting")
- return None
-
- # Validate content looks like markdown
- if not self._is_markdown(content):
- print(f"⚠️ Content doesn't look like markdown")
- return None
-
- return content
-
- except requests.RequestException as e:
- if attempt < self.max_retries - 1:
- # Calculate exponential backoff delay: 1s, 2s, 4s, etc.
- delay = 2 ** attempt
- print(f"⚠️ Attempt {attempt + 1}/{self.max_retries} failed: {e}")
- print(f" Retrying in {delay}s...")
- time.sleep(delay)
- else:
- print(f"❌ Failed to download {self.url} after {self.max_retries} attempts: {e}")
- return None
-
- return None
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_parser.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_parser.py
deleted file mode 100644
index e288c92..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/llms_txt_parser.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""ABOUTME: Parses llms.txt markdown content into structured page data"""
-"""ABOUTME: Extracts titles, content, code samples, and headings from markdown"""
-
-import re
-from typing import List, Dict
-
-class LlmsTxtParser:
- """Parse llms.txt markdown content into page structures"""
-
- def __init__(self, content: str):
- self.content = content
-
- def parse(self) -> List[Dict]:
- """
- Parse markdown content into page structures.
-
- Returns:
- List of page dicts with title, content, code_samples, headings
- """
- pages = []
-
- # Split by h1 headers (# Title)
- sections = re.split(r'\n# ', self.content)
-
- for section in sections:
- if not section.strip():
- continue
-
- # First line is title
- lines = section.split('\n')
- title = lines[0].strip('#').strip()
-
- # Parse content
- page = self._parse_section('\n'.join(lines[1:]), title)
- pages.append(page)
-
- return pages
-
- def _parse_section(self, content: str, title: str) -> Dict:
- """Parse a single section into page structure"""
- page = {
- 'title': title,
- 'content': '',
- 'code_samples': [],
- 'headings': [],
- 'url': f'llms-txt#{title.lower().replace(" ", "-")}',
- 'links': []
- }
-
- # Extract code blocks
- code_blocks = re.findall(r'```(\w+)?\n(.*?)```', content, re.DOTALL)
- for lang, code in code_blocks:
- page['code_samples'].append({
- 'code': code.strip(),
- 'language': lang or 'unknown'
- })
-
- # Extract h2/h3 headings
- headings = re.findall(r'^(#{2,3})\s+(.+)$', content, re.MULTILINE)
- for level_markers, text in headings:
- page['headings'].append({
- 'level': f'h{len(level_markers)}',
- 'text': text.strip(),
- 'id': text.lower().replace(' ', '-')
- })
-
- # Remove code blocks from content for plain text
- content_no_code = re.sub(r'```.*?```', '', content, flags=re.DOTALL)
-
- # Extract paragraphs
- paragraphs = [p.strip() for p in content_no_code.split('\n\n') if len(p.strip()) > 20]
- page['content'] = '\n\n'.join(paragraphs)
-
- return page
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/main.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/main.py
deleted file mode 100644
index dcf677d..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/main.py
+++ /dev/null
@@ -1,285 +0,0 @@
-#!/usr/bin/env python3
-"""
-Skill Seekers - Unified CLI Entry Point
-
-Provides a git-style unified command-line interface for all Skill Seekers tools.
-
-Usage:
- skill-seekers [options]
-
-Commands:
- scrape Scrape documentation website
- github Scrape GitHub repository
- pdf Extract from PDF file
- unified Multi-source scraping (docs + GitHub + PDF)
- enhance AI-powered enhancement (local, no API key)
- package Package skill into .zip file
- upload Upload skill to Claude
- estimate Estimate page count before scraping
-
-Examples:
- skill-seekers scrape --config configs/react.json
- skill-seekers github --repo microsoft/TypeScript
- skill-seekers unified --config configs/react_unified.json
- skill-seekers package output/react/
-"""
-
-import sys
-import argparse
-from typing import List, Optional
-
-
-def create_parser() -> argparse.ArgumentParser:
- """Create the main argument parser with subcommands."""
- parser = argparse.ArgumentParser(
- prog="skill-seekers",
- description="Convert documentation, GitHub repos, and PDFs into Claude AI skills",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Scrape documentation
- skill-seekers scrape --config configs/react.json
-
- # Scrape GitHub repository
- skill-seekers github --repo microsoft/TypeScript --name typescript
-
- # Multi-source scraping (unified)
- skill-seekers unified --config configs/react_unified.json
-
- # AI-powered enhancement
- skill-seekers enhance output/react/
-
- # Package and upload
- skill-seekers package output/react/
- skill-seekers upload output/react.zip
-
-For more information: https://github.com/yusufkaraaslan/Skill_Seekers
- """
- )
-
- parser.add_argument(
- "--version",
- action="version",
- version="%(prog)s 2.1.1"
- )
-
- subparsers = parser.add_subparsers(
- dest="command",
- title="commands",
- description="Available Skill Seekers commands",
- help="Command to run"
- )
-
- # === scrape subcommand ===
- scrape_parser = subparsers.add_parser(
- "scrape",
- help="Scrape documentation website",
- description="Scrape documentation website and generate skill"
- )
- scrape_parser.add_argument("--config", help="Config JSON file")
- scrape_parser.add_argument("--name", help="Skill name")
- scrape_parser.add_argument("--url", help="Documentation URL")
- scrape_parser.add_argument("--description", help="Skill description")
- scrape_parser.add_argument("--skip-scrape", action="store_true", help="Skip scraping, use cached data")
- scrape_parser.add_argument("--enhance", action="store_true", help="AI enhancement (API)")
- scrape_parser.add_argument("--enhance-local", action="store_true", help="AI enhancement (local)")
- scrape_parser.add_argument("--dry-run", action="store_true", help="Dry run mode")
- scrape_parser.add_argument("--async", dest="async_mode", action="store_true", help="Use async scraping")
- scrape_parser.add_argument("--workers", type=int, help="Number of async workers")
-
- # === github subcommand ===
- github_parser = subparsers.add_parser(
- "github",
- help="Scrape GitHub repository",
- description="Scrape GitHub repository and generate skill"
- )
- github_parser.add_argument("--config", help="Config JSON file")
- github_parser.add_argument("--repo", help="GitHub repo (owner/repo)")
- github_parser.add_argument("--name", help="Skill name")
- github_parser.add_argument("--description", help="Skill description")
-
- # === pdf subcommand ===
- pdf_parser = subparsers.add_parser(
- "pdf",
- help="Extract from PDF file",
- description="Extract content from PDF and generate skill"
- )
- pdf_parser.add_argument("--config", help="Config JSON file")
- pdf_parser.add_argument("--pdf", help="PDF file path")
- pdf_parser.add_argument("--name", help="Skill name")
- pdf_parser.add_argument("--description", help="Skill description")
- pdf_parser.add_argument("--from-json", help="Build from extracted JSON")
-
- # === unified subcommand ===
- unified_parser = subparsers.add_parser(
- "unified",
- help="Multi-source scraping (docs + GitHub + PDF)",
- description="Combine multiple sources into one skill"
- )
- unified_parser.add_argument("--config", required=True, help="Unified config JSON file")
- unified_parser.add_argument("--merge-mode", help="Merge mode (rule-based, claude-enhanced)")
- unified_parser.add_argument("--dry-run", action="store_true", help="Dry run mode")
-
- # === enhance subcommand ===
- enhance_parser = subparsers.add_parser(
- "enhance",
- help="AI-powered enhancement (local, no API key)",
- description="Enhance SKILL.md using Claude Code (local)"
- )
- enhance_parser.add_argument("skill_directory", help="Skill directory path")
-
- # === package subcommand ===
- package_parser = subparsers.add_parser(
- "package",
- help="Package skill into .zip file",
- description="Package skill directory into uploadable .zip"
- )
- package_parser.add_argument("skill_directory", help="Skill directory path")
- package_parser.add_argument("--no-open", action="store_true", help="Don't open output folder")
- package_parser.add_argument("--upload", action="store_true", help="Auto-upload after packaging")
-
- # === upload subcommand ===
- upload_parser = subparsers.add_parser(
- "upload",
- help="Upload skill to Claude",
- description="Upload .zip file to Claude via Anthropic API"
- )
- upload_parser.add_argument("zip_file", help=".zip file to upload")
- upload_parser.add_argument("--api-key", help="Anthropic API key")
-
- # === estimate subcommand ===
- estimate_parser = subparsers.add_parser(
- "estimate",
- help="Estimate page count before scraping",
- description="Estimate total pages for documentation scraping"
- )
- estimate_parser.add_argument("config", help="Config JSON file")
- estimate_parser.add_argument("--max-discovery", type=int, help="Max pages to discover")
-
- return parser
-
-
-def main(argv: Optional[List[str]] = None) -> int:
- """Main entry point for the unified CLI.
-
- Args:
- argv: Command-line arguments (defaults to sys.argv)
-
- Returns:
- Exit code (0 for success, non-zero for error)
- """
- parser = create_parser()
- args = parser.parse_args(argv)
-
- if not args.command:
- parser.print_help()
- return 1
-
- # Delegate to the appropriate tool
- try:
- if args.command == "scrape":
- from skill_seekers.cli.doc_scraper import main as scrape_main
- # Convert args namespace to sys.argv format for doc_scraper
- sys.argv = ["doc_scraper.py"]
- if args.config:
- sys.argv.extend(["--config", args.config])
- if args.name:
- sys.argv.extend(["--name", args.name])
- if args.url:
- sys.argv.extend(["--url", args.url])
- if args.description:
- sys.argv.extend(["--description", args.description])
- if args.skip_scrape:
- sys.argv.append("--skip-scrape")
- if args.enhance:
- sys.argv.append("--enhance")
- if args.enhance_local:
- sys.argv.append("--enhance-local")
- if args.dry_run:
- sys.argv.append("--dry-run")
- if args.async_mode:
- sys.argv.append("--async")
- if args.workers:
- sys.argv.extend(["--workers", str(args.workers)])
- return scrape_main() or 0
-
- elif args.command == "github":
- from skill_seekers.cli.github_scraper import main as github_main
- sys.argv = ["github_scraper.py"]
- if args.config:
- sys.argv.extend(["--config", args.config])
- if args.repo:
- sys.argv.extend(["--repo", args.repo])
- if args.name:
- sys.argv.extend(["--name", args.name])
- if args.description:
- sys.argv.extend(["--description", args.description])
- return github_main() or 0
-
- elif args.command == "pdf":
- from skill_seekers.cli.pdf_scraper import main as pdf_main
- sys.argv = ["pdf_scraper.py"]
- if args.config:
- sys.argv.extend(["--config", args.config])
- if args.pdf:
- sys.argv.extend(["--pdf", args.pdf])
- if args.name:
- sys.argv.extend(["--name", args.name])
- if args.description:
- sys.argv.extend(["--description", args.description])
- if args.from_json:
- sys.argv.extend(["--from-json", args.from_json])
- return pdf_main() or 0
-
- elif args.command == "unified":
- from skill_seekers.cli.unified_scraper import main as unified_main
- sys.argv = ["unified_scraper.py", "--config", args.config]
- if args.merge_mode:
- sys.argv.extend(["--merge-mode", args.merge_mode])
- if args.dry_run:
- sys.argv.append("--dry-run")
- return unified_main() or 0
-
- elif args.command == "enhance":
- from skill_seekers.cli.enhance_skill_local import main as enhance_main
- sys.argv = ["enhance_skill_local.py", args.skill_directory]
- return enhance_main() or 0
-
- elif args.command == "package":
- from skill_seekers.cli.package_skill import main as package_main
- sys.argv = ["package_skill.py", args.skill_directory]
- if args.no_open:
- sys.argv.append("--no-open")
- if args.upload:
- sys.argv.append("--upload")
- return package_main() or 0
-
- elif args.command == "upload":
- from skill_seekers.cli.upload_skill import main as upload_main
- sys.argv = ["upload_skill.py", args.zip_file]
- if args.api_key:
- sys.argv.extend(["--api-key", args.api_key])
- return upload_main() or 0
-
- elif args.command == "estimate":
- from skill_seekers.cli.estimate_pages import main as estimate_main
- sys.argv = ["estimate_pages.py", args.config]
- if args.max_discovery:
- sys.argv.extend(["--max-discovery", str(args.max_discovery)])
- return estimate_main() or 0
-
- else:
- print(f"Error: Unknown command '{args.command}'", file=sys.stderr)
- parser.print_help()
- return 1
-
- except KeyboardInterrupt:
- print("\n\nInterrupted by user", file=sys.stderr)
- return 130
- except Exception as e:
- print(f"Error: {e}", file=sys.stderr)
- return 1
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/merge_sources.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/merge_sources.py
deleted file mode 100644
index 552ac82..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/merge_sources.py
+++ /dev/null
@@ -1,513 +0,0 @@
-#!/usr/bin/env python3
-"""
-Source Merger for Multi-Source Skills
-
-Merges documentation and code data intelligently:
-- Rule-based merge: Fast, deterministic rules
-- Claude-enhanced merge: AI-powered reconciliation
-
-Handles conflicts and creates unified API reference.
-"""
-
-import json
-import logging
-import subprocess
-import tempfile
-import os
-from pathlib import Path
-from typing import Dict, List, Any, Optional
-from .conflict_detector import Conflict, ConflictDetector
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-class RuleBasedMerger:
- """
- Rule-based API merger using deterministic rules.
-
- Rules:
- 1. If API only in docs → Include with [DOCS_ONLY] tag
- 2. If API only in code → Include with [UNDOCUMENTED] tag
- 3. If both match perfectly → Include normally
- 4. If conflict → Include both versions with [CONFLICT] tag, prefer code signature
- """
-
- def __init__(self, docs_data: Dict, github_data: Dict, conflicts: List[Conflict]):
- """
- Initialize rule-based merger.
-
- Args:
- docs_data: Documentation scraper data
- github_data: GitHub scraper data
- conflicts: List of detected conflicts
- """
- self.docs_data = docs_data
- self.github_data = github_data
- self.conflicts = conflicts
-
- # Build conflict index for fast lookup
- self.conflict_index = {c.api_name: c for c in conflicts}
-
- # Extract APIs from both sources
- detector = ConflictDetector(docs_data, github_data)
- self.docs_apis = detector.docs_apis
- self.code_apis = detector.code_apis
-
- def merge_all(self) -> Dict[str, Any]:
- """
- Merge all APIs using rule-based logic.
-
- Returns:
- Dict containing merged API data
- """
- logger.info("Starting rule-based merge...")
-
- merged_apis = {}
-
- # Get all unique API names
- all_api_names = set(self.docs_apis.keys()) | set(self.code_apis.keys())
-
- for api_name in sorted(all_api_names):
- merged_api = self._merge_single_api(api_name)
- merged_apis[api_name] = merged_api
-
- logger.info(f"Merged {len(merged_apis)} APIs")
-
- return {
- 'merge_mode': 'rule-based',
- 'apis': merged_apis,
- 'summary': {
- 'total_apis': len(merged_apis),
- 'docs_only': sum(1 for api in merged_apis.values() if api['status'] == 'docs_only'),
- 'code_only': sum(1 for api in merged_apis.values() if api['status'] == 'code_only'),
- 'matched': sum(1 for api in merged_apis.values() if api['status'] == 'matched'),
- 'conflict': sum(1 for api in merged_apis.values() if api['status'] == 'conflict')
- }
- }
-
- def _merge_single_api(self, api_name: str) -> Dict[str, Any]:
- """
- Merge a single API using rules.
-
- Args:
- api_name: Name of the API to merge
-
- Returns:
- Merged API dict
- """
- in_docs = api_name in self.docs_apis
- in_code = api_name in self.code_apis
- has_conflict = api_name in self.conflict_index
-
- # Rule 1: Only in docs
- if in_docs and not in_code:
- conflict = self.conflict_index.get(api_name)
- return {
- 'name': api_name,
- 'status': 'docs_only',
- 'source': 'documentation',
- 'data': self.docs_apis[api_name],
- 'warning': 'This API is documented but not found in codebase',
- 'conflict': conflict.__dict__ if conflict else None
- }
-
- # Rule 2: Only in code
- if in_code and not in_docs:
- is_private = api_name.startswith('_')
- conflict = self.conflict_index.get(api_name)
- return {
- 'name': api_name,
- 'status': 'code_only',
- 'source': 'code',
- 'data': self.code_apis[api_name],
- 'warning': 'This API exists in code but is not documented' if not is_private else 'Internal/private API',
- 'conflict': conflict.__dict__ if conflict else None
- }
-
- # Both exist - check for conflicts
- docs_info = self.docs_apis[api_name]
- code_info = self.code_apis[api_name]
-
- # Rule 3: Both match perfectly (no conflict)
- if not has_conflict:
- return {
- 'name': api_name,
- 'status': 'matched',
- 'source': 'both',
- 'docs_data': docs_info,
- 'code_data': code_info,
- 'merged_signature': self._create_merged_signature(code_info, docs_info),
- 'merged_description': docs_info.get('docstring') or code_info.get('docstring')
- }
-
- # Rule 4: Conflict exists - prefer code signature, keep docs description
- conflict = self.conflict_index[api_name]
-
- return {
- 'name': api_name,
- 'status': 'conflict',
- 'source': 'both',
- 'docs_data': docs_info,
- 'code_data': code_info,
- 'conflict': conflict.__dict__,
- 'resolution': 'prefer_code_signature',
- 'merged_signature': self._create_merged_signature(code_info, docs_info),
- 'merged_description': docs_info.get('docstring') or code_info.get('docstring'),
- 'warning': conflict.difference
- }
-
- def _create_merged_signature(self, code_info: Dict, docs_info: Dict) -> str:
- """
- Create merged signature preferring code data.
-
- Args:
- code_info: API info from code
- docs_info: API info from docs
-
- Returns:
- Merged signature string
- """
- name = code_info.get('name', docs_info.get('name'))
- params = code_info.get('parameters', docs_info.get('parameters', []))
- return_type = code_info.get('return_type', docs_info.get('return_type'))
-
- # Build parameter string
- param_strs = []
- for param in params:
- param_str = param['name']
- if param.get('type_hint'):
- param_str += f": {param['type_hint']}"
- if param.get('default'):
- param_str += f" = {param['default']}"
- param_strs.append(param_str)
-
- signature = f"{name}({', '.join(param_strs)})"
-
- if return_type:
- signature += f" -> {return_type}"
-
- return signature
-
-
-class ClaudeEnhancedMerger:
- """
- Claude-enhanced API merger using local Claude Code.
-
- Opens Claude Code in a new terminal to intelligently reconcile conflicts.
- Uses the same approach as enhance_skill_local.py.
- """
-
- def __init__(self, docs_data: Dict, github_data: Dict, conflicts: List[Conflict]):
- """
- Initialize Claude-enhanced merger.
-
- Args:
- docs_data: Documentation scraper data
- github_data: GitHub scraper data
- conflicts: List of detected conflicts
- """
- self.docs_data = docs_data
- self.github_data = github_data
- self.conflicts = conflicts
-
- # First do rule-based merge as baseline
- self.rule_merger = RuleBasedMerger(docs_data, github_data, conflicts)
-
- def merge_all(self) -> Dict[str, Any]:
- """
- Merge all APIs using Claude enhancement.
-
- Returns:
- Dict containing merged API data
- """
- logger.info("Starting Claude-enhanced merge...")
-
- # Create temporary workspace
- workspace_dir = self._create_workspace()
-
- # Launch Claude Code for enhancement
- logger.info("Launching Claude Code for intelligent merging...")
- logger.info("Claude will analyze conflicts and create reconciled API reference")
-
- try:
- self._launch_claude_merge(workspace_dir)
-
- # Read enhanced results
- merged_data = self._read_merged_results(workspace_dir)
-
- logger.info("Claude-enhanced merge complete")
- return merged_data
-
- except Exception as e:
- logger.error(f"Claude enhancement failed: {e}")
- logger.info("Falling back to rule-based merge")
- return self.rule_merger.merge_all()
-
- def _create_workspace(self) -> str:
- """
- Create temporary workspace with merge context.
-
- Returns:
- Path to workspace directory
- """
- workspace = tempfile.mkdtemp(prefix='skill_merge_')
- logger.info(f"Created merge workspace: {workspace}")
-
- # Write context files for Claude
- self._write_context_files(workspace)
-
- return workspace
-
- def _write_context_files(self, workspace: str):
- """Write context files for Claude to analyze."""
-
- # 1. Write conflicts summary
- conflicts_file = os.path.join(workspace, 'conflicts.json')
- with open(conflicts_file, 'w') as f:
- json.dump({
- 'conflicts': [c.__dict__ for c in self.conflicts],
- 'summary': {
- 'total': len(self.conflicts),
- 'by_type': self._count_by_field('type'),
- 'by_severity': self._count_by_field('severity')
- }
- }, f, indent=2)
-
- # 2. Write documentation APIs
- docs_apis_file = os.path.join(workspace, 'docs_apis.json')
- detector = ConflictDetector(self.docs_data, self.github_data)
- with open(docs_apis_file, 'w') as f:
- json.dump(detector.docs_apis, f, indent=2)
-
- # 3. Write code APIs
- code_apis_file = os.path.join(workspace, 'code_apis.json')
- with open(code_apis_file, 'w') as f:
- json.dump(detector.code_apis, f, indent=2)
-
- # 4. Write merge instructions for Claude
- instructions = """# API Merge Task
-
-You are merging API documentation from two sources:
-1. Official documentation (user-facing)
-2. Source code analysis (implementation reality)
-
-## Context Files:
-- `conflicts.json` - All detected conflicts between sources
-- `docs_apis.json` - APIs from documentation
-- `code_apis.json` - APIs from source code
-
-## Your Task:
-For each conflict, reconcile the differences intelligently:
-
-1. **Prefer code signatures as source of truth**
- - Use actual parameter names, types, defaults from code
- - Code is what actually runs, docs might be outdated
-
-2. **Keep documentation descriptions**
- - Docs are user-friendly, code comments might be technical
- - Keep the docs' explanation of what the API does
-
-3. **Add implementation notes for discrepancies**
- - If docs differ from code, explain the difference
- - Example: "⚠️ The `snap` parameter exists in code but is not documented"
-
-4. **Flag missing APIs clearly**
- - Missing in docs → Add [UNDOCUMENTED] tag
- - Missing in code → Add [REMOVED] or [DOCS_ERROR] tag
-
-5. **Create unified API reference**
- - One definitive signature per API
- - Clear warnings about conflicts
- - Implementation notes where helpful
-
-## Output Format:
-Create `merged_apis.json` with this structure:
-
-```json
-{
- "apis": {
- "API.name": {
- "signature": "final_signature_here",
- "parameters": [...],
- "return_type": "type",
- "description": "user-friendly description",
- "implementation_notes": "Any discrepancies or warnings",
- "source": "both|docs_only|code_only",
- "confidence": "high|medium|low"
- }
- }
-}
-```
-
-Take your time to analyze each conflict carefully. The goal is to create the most accurate and helpful API reference possible.
-"""
-
- instructions_file = os.path.join(workspace, 'MERGE_INSTRUCTIONS.md')
- with open(instructions_file, 'w') as f:
- f.write(instructions)
-
- logger.info(f"Wrote context files to {workspace}")
-
- def _count_by_field(self, field: str) -> Dict[str, int]:
- """Count conflicts by a specific field."""
- counts = {}
- for conflict in self.conflicts:
- value = getattr(conflict, field)
- counts[value] = counts.get(value, 0) + 1
- return counts
-
- def _launch_claude_merge(self, workspace: str):
- """
- Launch Claude Code to perform merge.
-
- Similar to enhance_skill_local.py approach.
- """
- # Create a script that Claude will execute
- script_path = os.path.join(workspace, 'merge_script.sh')
-
- script_content = f"""#!/bin/bash
-# Automatic merge script for Claude Code
-
-cd "{workspace}"
-
-echo "📊 Analyzing conflicts..."
-cat conflicts.json | head -20
-
-echo ""
-echo "📖 Documentation APIs: $(cat docs_apis.json | grep -c '\"name\"')"
-echo "💻 Code APIs: $(cat code_apis.json | grep -c '\"name\"')"
-echo ""
-echo "Please review the conflicts and create merged_apis.json"
-echo "Follow the instructions in MERGE_INSTRUCTIONS.md"
-echo ""
-echo "When done, save merged_apis.json and close this terminal."
-
-# Wait for user to complete merge
-read -p "Press Enter when merge is complete..."
-"""
-
- with open(script_path, 'w') as f:
- f.write(script_content)
-
- os.chmod(script_path, 0o755)
-
- # Open new terminal with Claude Code
- # Try different terminal emulators
- terminals = [
- ['x-terminal-emulator', '-e'],
- ['gnome-terminal', '--'],
- ['xterm', '-e'],
- ['konsole', '-e']
- ]
-
- for terminal_cmd in terminals:
- try:
- cmd = terminal_cmd + ['bash', script_path]
- subprocess.Popen(cmd)
- logger.info(f"Opened terminal with {terminal_cmd[0]}")
- break
- except FileNotFoundError:
- continue
-
- # Wait for merge to complete
- merged_file = os.path.join(workspace, 'merged_apis.json')
- logger.info(f"Waiting for merged results at: {merged_file}")
- logger.info("Close the terminal when done to continue...")
-
- # Poll for file existence
- import time
- timeout = 3600 # 1 hour max
- elapsed = 0
- while not os.path.exists(merged_file) and elapsed < timeout:
- time.sleep(5)
- elapsed += 5
-
- if not os.path.exists(merged_file):
- raise TimeoutError("Claude merge timed out after 1 hour")
-
- def _read_merged_results(self, workspace: str) -> Dict[str, Any]:
- """Read merged results from workspace."""
- merged_file = os.path.join(workspace, 'merged_apis.json')
-
- if not os.path.exists(merged_file):
- raise FileNotFoundError(f"Merged results not found: {merged_file}")
-
- with open(merged_file, 'r') as f:
- merged_data = json.load(f)
-
- return {
- 'merge_mode': 'claude-enhanced',
- **merged_data
- }
-
-
-def merge_sources(docs_data_path: str,
- github_data_path: str,
- output_path: str,
- mode: str = 'rule-based') -> Dict[str, Any]:
- """
- Merge documentation and GitHub data.
-
- Args:
- docs_data_path: Path to documentation data JSON
- github_data_path: Path to GitHub data JSON
- output_path: Path to save merged output
- mode: 'rule-based' or 'claude-enhanced'
-
- Returns:
- Merged data dict
- """
- # Load data
- with open(docs_data_path, 'r') as f:
- docs_data = json.load(f)
-
- with open(github_data_path, 'r') as f:
- github_data = json.load(f)
-
- # Detect conflicts
- detector = ConflictDetector(docs_data, github_data)
- conflicts = detector.detect_all_conflicts()
-
- logger.info(f"Detected {len(conflicts)} conflicts")
-
- # Merge based on mode
- if mode == 'claude-enhanced':
- merger = ClaudeEnhancedMerger(docs_data, github_data, conflicts)
- else:
- merger = RuleBasedMerger(docs_data, github_data, conflicts)
-
- merged_data = merger.merge_all()
-
- # Save merged data
- with open(output_path, 'w') as f:
- json.dump(merged_data, f, indent=2, ensure_ascii=False)
-
- logger.info(f"Merged data saved to: {output_path}")
-
- return merged_data
-
-
-if __name__ == '__main__':
- import argparse
-
- parser = argparse.ArgumentParser(description='Merge documentation and code sources')
- parser.add_argument('docs_data', help='Path to documentation data JSON')
- parser.add_argument('github_data', help='Path to GitHub data JSON')
- parser.add_argument('--output', '-o', default='merged_data.json', help='Output file path')
- parser.add_argument('--mode', '-m', choices=['rule-based', 'claude-enhanced'],
- default='rule-based', help='Merge mode')
-
- args = parser.parse_args()
-
- merged = merge_sources(args.docs_data, args.github_data, args.output, args.mode)
-
- # Print summary
- summary = merged.get('summary', {})
- print(f"\n✅ Merge complete ({merged.get('merge_mode')})")
- print(f" Total APIs: {summary.get('total_apis', 0)}")
- print(f" Matched: {summary.get('matched', 0)}")
- print(f" Docs only: {summary.get('docs_only', 0)}")
- print(f" Code only: {summary.get('code_only', 0)}")
- print(f" Conflicts: {summary.get('conflict', 0)}")
- print(f"\n📄 Saved to: {args.output}")
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/package_multi.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/package_multi.py
deleted file mode 100644
index bffdb9c..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/package_multi.py
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env python3
-"""
-Multi-Skill Packager
-
-Package multiple skills at once. Useful for packaging router + sub-skills together.
-"""
-
-import sys
-import argparse
-from pathlib import Path
-import subprocess
-
-
-def package_skill(skill_dir: Path) -> bool:
- """Package a single skill"""
- try:
- result = subprocess.run(
- [sys.executable, str(Path(__file__).parent / "package_skill.py"), str(skill_dir)],
- capture_output=True,
- text=True
- )
- return result.returncode == 0
- except Exception as e:
- print(f"❌ Error packaging {skill_dir}: {e}")
- return False
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Package multiple skills at once",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Package all godot skills
- python3 package_multi.py output/godot*/
-
- # Package specific skills
- python3 package_multi.py output/godot-2d/ output/godot-3d/ output/godot-scripting/
- """
- )
-
- parser.add_argument(
- 'skill_dirs',
- nargs='+',
- help='Skill directories to package'
- )
-
- args = parser.parse_args()
-
- print(f"\n{'='*60}")
- print(f"MULTI-SKILL PACKAGER")
- print(f"{'='*60}\n")
-
- skill_dirs = [Path(d) for d in args.skill_dirs]
- success_count = 0
- total_count = len(skill_dirs)
-
- for skill_dir in skill_dirs:
- if not skill_dir.exists():
- print(f"⚠️ Skipping (not found): {skill_dir}")
- continue
-
- if not (skill_dir / "SKILL.md").exists():
- print(f"⚠️ Skipping (no SKILL.md): {skill_dir}")
- continue
-
- print(f"📦 Packaging: {skill_dir.name}")
- if package_skill(skill_dir):
- success_count += 1
- print(f" ✅ Success")
- else:
- print(f" ❌ Failed")
- print("")
-
- print(f"{'='*60}")
- print(f"SUMMARY: {success_count}/{total_count} skills packaged")
- print(f"{'='*60}\n")
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/package_skill.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/package_skill.py
deleted file mode 100644
index cf251d0..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/package_skill.py
+++ /dev/null
@@ -1,220 +0,0 @@
-#!/usr/bin/env python3
-"""
-Simple Skill Packager
-Packages a skill directory into a .zip file for Claude.
-
-Usage:
- skill-seekers package output/steam-inventory/
- skill-seekers package output/react/
- skill-seekers package output/react/ --no-open # Don't open folder
-"""
-
-import os
-import sys
-import zipfile
-import argparse
-from pathlib import Path
-
-# Import utilities
-try:
- from utils import (
- open_folder,
- print_upload_instructions,
- format_file_size,
- validate_skill_directory
- )
- from quality_checker import SkillQualityChecker, print_report
-except ImportError:
- # If running from different directory, add cli to path
- sys.path.insert(0, str(Path(__file__).parent))
- from utils import (
- open_folder,
- print_upload_instructions,
- format_file_size,
- validate_skill_directory
- )
- from quality_checker import SkillQualityChecker, print_report
-
-
-def package_skill(skill_dir, open_folder_after=True, skip_quality_check=False):
- """
- Package a skill directory into a .zip file
-
- Args:
- skill_dir: Path to skill directory
- open_folder_after: Whether to open the output folder after packaging
- skip_quality_check: Skip quality checks before packaging
-
- Returns:
- tuple: (success, zip_path) where success is bool and zip_path is Path or None
- """
- skill_path = Path(skill_dir)
-
- # Validate skill directory
- is_valid, error_msg = validate_skill_directory(skill_path)
- if not is_valid:
- print(f"❌ Error: {error_msg}")
- return False, None
-
- # Run quality checks (unless skipped)
- if not skip_quality_check:
- print("\n" + "=" * 60)
- print("QUALITY CHECK")
- print("=" * 60)
-
- checker = SkillQualityChecker(skill_path)
- report = checker.check_all()
-
- # Print report
- print_report(report, verbose=False)
-
- # If there are errors or warnings, ask user to confirm
- if report.has_errors or report.has_warnings:
- print("=" * 60)
- response = input("\nContinue with packaging? (y/n): ").strip().lower()
- if response != 'y':
- print("\n❌ Packaging cancelled by user")
- return False, None
- print()
- else:
- print("=" * 60)
- print()
-
- # Create zip filename
- skill_name = skill_path.name
- zip_path = skill_path.parent / f"{skill_name}.zip"
-
- print(f"📦 Packaging skill: {skill_name}")
- print(f" Source: {skill_path}")
- print(f" Output: {zip_path}")
-
- # Create zip file
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
- for root, dirs, files in os.walk(skill_path):
- # Skip backup files
- files = [f for f in files if not f.endswith('.backup')]
-
- for file in files:
- file_path = Path(root) / file
- arcname = file_path.relative_to(skill_path)
- zf.write(file_path, arcname)
- print(f" + {arcname}")
-
- # Get zip size
- zip_size = zip_path.stat().st_size
- print(f"\n✅ Package created: {zip_path}")
- print(f" Size: {zip_size:,} bytes ({format_file_size(zip_size)})")
-
- # Open folder in file browser
- if open_folder_after:
- print(f"\n📂 Opening folder: {zip_path.parent}")
- open_folder(zip_path.parent)
-
- # Print upload instructions
- print_upload_instructions(zip_path)
-
- return True, zip_path
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Package a skill directory into a .zip file for Claude",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Package skill with quality checks (recommended)
- skill-seekers package output/react/
-
- # Package skill without opening folder
- skill-seekers package output/react/ --no-open
-
- # Skip quality checks (faster, but not recommended)
- skill-seekers package output/react/ --skip-quality-check
-
- # Package and auto-upload to Claude
- skill-seekers package output/react/ --upload
-
- # Get help
- skill-seekers package --help
- """
- )
-
- parser.add_argument(
- 'skill_dir',
- help='Path to skill directory (e.g., output/react/)'
- )
-
- parser.add_argument(
- '--no-open',
- action='store_true',
- help='Do not open the output folder after packaging'
- )
-
- parser.add_argument(
- '--skip-quality-check',
- action='store_true',
- help='Skip quality checks before packaging'
- )
-
- parser.add_argument(
- '--upload',
- action='store_true',
- help='Automatically upload to Claude after packaging (requires ANTHROPIC_API_KEY)'
- )
-
- args = parser.parse_args()
-
- success, zip_path = package_skill(
- args.skill_dir,
- open_folder_after=not args.no_open,
- skip_quality_check=args.skip_quality_check
- )
-
- if not success:
- sys.exit(1)
-
- # Auto-upload if requested
- if args.upload:
- # Check if API key is set BEFORE attempting upload
- api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
-
- if not api_key:
- # No API key - show helpful message but DON'T fail
- print("\n" + "="*60)
- print("💡 Automatic Upload")
- print("="*60)
- print()
- print("To enable automatic upload:")
- print(" 1. Get API key from https://console.anthropic.com/")
- print(" 2. Set: export ANTHROPIC_API_KEY=sk-ant-...")
- print(" 3. Run package_skill.py with --upload flag")
- print()
- print("For now, use manual upload (instructions above) ☝️")
- print("="*60)
- # Exit successfully - packaging worked!
- sys.exit(0)
-
- # API key exists - try upload
- try:
- from upload_skill import upload_skill_api
- print("\n" + "="*60)
- upload_success, message = upload_skill_api(zip_path)
- if not upload_success:
- print(f"❌ Upload failed: {message}")
- print()
- print("💡 Try manual upload instead (instructions above) ☝️")
- print("="*60)
- # Exit successfully - packaging worked even if upload failed
- sys.exit(0)
- else:
- print("="*60)
- sys.exit(0)
- except ImportError:
- print("\n❌ Error: upload_skill.py not found")
- sys.exit(1)
-
- sys.exit(0)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/pdf_extractor_poc.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/pdf_extractor_poc.py
deleted file mode 100644
index f8c0fe8..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/pdf_extractor_poc.py
+++ /dev/null
@@ -1,1222 +0,0 @@
-#!/usr/bin/env python3
-"""
-PDF Text Extractor - Complete Feature Set (Tasks B1.2 + B1.3 + B1.4 + B1.5 + Priority 2 & 3)
-
-Extracts text, code blocks, and images from PDF documentation files.
-Uses PyMuPDF (fitz) for fast, high-quality extraction.
-
-Features:
- - Text and markdown extraction
- - Code block detection (font, indent, pattern)
- - Language detection with confidence scoring (19+ languages) (B1.4)
- - Syntax validation and quality scoring (B1.4)
- - Quality statistics and filtering (B1.4)
- - Image extraction to files (B1.5)
- - Image filtering by size (B1.5)
- - Page chunking and chapter detection (B1.3)
- - Code block merging across pages (B1.3)
-
-Advanced Features (Priority 2 & 3):
- - OCR support for scanned PDFs (requires pytesseract) (Priority 2)
- - Password-protected PDF support (Priority 2)
- - Table extraction (Priority 2)
- - Parallel page processing (Priority 3)
- - Caching of expensive operations (Priority 3)
-
-Usage:
- # Basic extraction
- python3 pdf_extractor_poc.py input.pdf
- python3 pdf_extractor_poc.py input.pdf --output output.json
- python3 pdf_extractor_poc.py input.pdf --verbose
-
- # Quality filtering
- python3 pdf_extractor_poc.py input.pdf --min-quality 5.0
-
- # Image extraction
- python3 pdf_extractor_poc.py input.pdf --extract-images
- python3 pdf_extractor_poc.py input.pdf --extract-images --image-dir images/
-
- # Advanced features
- python3 pdf_extractor_poc.py scanned.pdf --ocr
- python3 pdf_extractor_poc.py encrypted.pdf --password mypassword
- python3 pdf_extractor_poc.py input.pdf --extract-tables
- python3 pdf_extractor_poc.py large.pdf --parallel --workers 8
-
-Example:
- python3 pdf_extractor_poc.py docs/manual.pdf -o output.json -v \
- --chunk-size 15 --min-quality 6.0 --extract-images \
- --extract-tables --parallel
-"""
-
-import os
-import sys
-import json
-import re
-import argparse
-from pathlib import Path
-
-# Check if PyMuPDF is installed
-try:
- import fitz # PyMuPDF
-except ImportError:
- print("ERROR: PyMuPDF not installed")
- print("Install with: pip install PyMuPDF")
- sys.exit(1)
-
-# Optional dependencies for advanced features
-try:
- import pytesseract
- from PIL import Image
- TESSERACT_AVAILABLE = True
-except ImportError:
- TESSERACT_AVAILABLE = False
-
-try:
- import concurrent.futures
- CONCURRENT_AVAILABLE = True
-except ImportError:
- CONCURRENT_AVAILABLE = False
-
-
-class PDFExtractor:
- """Extract text and code from PDF documentation"""
-
- def __init__(self, pdf_path, verbose=False, chunk_size=10, min_quality=0.0,
- extract_images=False, image_dir=None, min_image_size=100,
- use_ocr=False, password=None, extract_tables=False,
- parallel=False, max_workers=None, use_cache=True):
- self.pdf_path = pdf_path
- self.verbose = verbose
- self.chunk_size = chunk_size # Pages per chunk (0 = no chunking)
- self.min_quality = min_quality # Minimum quality score (0-10)
- self.extract_images = extract_images # Extract images to files (NEW in B1.5)
- self.image_dir = image_dir # Directory to save images (NEW in B1.5)
- self.min_image_size = min_image_size # Minimum image dimension (NEW in B1.5)
-
- # Advanced features (Priority 2 & 3)
- self.use_ocr = use_ocr # OCR for scanned PDFs (Priority 2)
- self.password = password # Password for encrypted PDFs (Priority 2)
- self.extract_tables = extract_tables # Extract tables (Priority 2)
- self.parallel = parallel # Parallel processing (Priority 3)
- self.max_workers = max_workers or os.cpu_count() # Worker threads (Priority 3)
- self.use_cache = use_cache # Cache expensive operations (Priority 3)
-
- self.doc = None
- self.pages = []
- self.chapters = [] # Detected chapters/sections
- self.extracted_images = [] # List of extracted image info (NEW in B1.5)
- self._cache = {} # Cache for expensive operations (Priority 3)
-
- def log(self, message):
- """Print message if verbose mode enabled"""
- if self.verbose:
- print(message)
-
- def extract_text_with_ocr(self, page):
- """
- Extract text from scanned PDF page using OCR (Priority 2).
- Falls back to regular text extraction if OCR is not available.
-
- Args:
- page: PyMuPDF page object
-
- Returns:
- str: Extracted text
- """
- # Try regular text extraction first
- text = page.get_text("text").strip()
-
- # If page has very little text, it might be scanned
- if len(text) < 50 and self.use_ocr:
- if not TESSERACT_AVAILABLE:
- self.log("⚠️ OCR requested but pytesseract not installed")
- self.log(" Install with: pip install pytesseract Pillow")
- return text
-
- try:
- # Render page as image
- pix = page.get_pixmap()
- img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
-
- # Run OCR
- ocr_text = pytesseract.image_to_string(img)
- self.log(f" OCR extracted {len(ocr_text)} chars (was {len(text)})")
- return ocr_text if len(ocr_text) > len(text) else text
-
- except Exception as e:
- self.log(f" OCR failed: {e}")
- return text
-
- return text
-
- def extract_tables_from_page(self, page):
- """
- Extract tables from PDF page (Priority 2).
- Uses PyMuPDF's table detection.
-
- Args:
- page: PyMuPDF page object
-
- Returns:
- list: List of extracted tables as dicts
- """
- if not self.extract_tables:
- return []
-
- tables = []
- try:
- # PyMuPDF table extraction
- tabs = page.find_tables()
- for idx, tab in enumerate(tabs.tables):
- table_data = {
- 'table_index': idx,
- 'rows': tab.extract(),
- 'bbox': tab.bbox,
- 'row_count': len(tab.extract()),
- 'col_count': len(tab.extract()[0]) if tab.extract() else 0
- }
- tables.append(table_data)
- self.log(f" Found table {idx}: {table_data['row_count']}x{table_data['col_count']}")
-
- except Exception as e:
- self.log(f" Table extraction failed: {e}")
-
- return tables
-
- def get_cached(self, key):
- """
- Get cached value (Priority 3).
-
- Args:
- key: Cache key
-
- Returns:
- Cached value or None
- """
- if not self.use_cache:
- return None
- return self._cache.get(key)
-
- def set_cached(self, key, value):
- """
- Set cached value (Priority 3).
-
- Args:
- key: Cache key
- value: Value to cache
- """
- if self.use_cache:
- self._cache[key] = value
-
- def detect_language_from_code(self, code):
- """
- Detect programming language from code content using patterns.
- Enhanced in B1.4 with confidence scoring.
-
- Returns (language, confidence) tuple
- """
- code_lower = code.lower()
-
- # Language detection patterns with weights
- patterns = {
- 'python': [
- (r'\bdef\s+\w+\s*\(', 3),
- (r'\bimport\s+\w+', 2),
- (r'\bclass\s+\w+:', 3),
- (r'\bfrom\s+\w+\s+import', 2),
- (r':\s*$', 1), # Lines ending with :
- (r'^\s{4}|\t', 1), # Indentation
- ],
- 'javascript': [
- (r'\bfunction\s+\w+\s*\(', 3),
- (r'\bconst\s+\w+\s*=', 2),
- (r'\blet\s+\w+\s*=', 2),
- (r'=>', 2),
- (r'\bconsole\.log', 2),
- (r'\bvar\s+\w+\s*=', 1),
- ],
- 'java': [
- (r'\bpublic\s+class\s+\w+', 4),
- (r'\bprivate\s+\w+\s+\w+', 2),
- (r'\bSystem\.out\.println', 3),
- (r'\bpublic\s+static\s+void', 3),
- ],
- 'cpp': [
- (r'#include\s*<', 3),
- (r'\bstd::', 3),
- (r'\bnamespace\s+\w+', 2),
- (r'cout\s*<<', 3),
- (r'\bvoid\s+\w+\s*\(', 1),
- ],
- 'c': [
- (r'#include\s+<\w+\.h>', 4),
- (r'\bprintf\s*\(', 3),
- (r'\bmain\s*\(', 2),
- (r'\bstruct\s+\w+', 2),
- ],
- 'csharp': [
- (r'\bnamespace\s+\w+', 3),
- (r'\bpublic\s+class\s+\w+', 3),
- (r'\busing\s+System', 3),
- ],
- 'go': [
- (r'\bfunc\s+\w+\s*\(', 3),
- (r'\bpackage\s+\w+', 4),
- (r':=', 2),
- (r'\bfmt\.Print', 2),
- ],
- 'rust': [
- (r'\bfn\s+\w+\s*\(', 4),
- (r'\blet\s+mut\s+\w+', 3),
- (r'\bprintln!', 3),
- (r'\bimpl\s+\w+', 2),
- ],
- 'php': [
- (r'<\?php', 5),
- (r'\$\w+\s*=', 2),
- (r'\bfunction\s+\w+\s*\(', 1),
- ],
- 'ruby': [
- (r'\bdef\s+\w+', 3),
- (r'\bend\b', 2),
- (r'\brequire\s+[\'"]', 2),
- ],
- 'swift': [
- (r'\bfunc\s+\w+\s*\(', 3),
- (r'\bvar\s+\w+:', 2),
- (r'\blet\s+\w+:', 2),
- ],
- 'kotlin': [
- (r'\bfun\s+\w+\s*\(', 4),
- (r'\bval\s+\w+\s*=', 2),
- (r'\bvar\s+\w+\s*=', 2),
- ],
- 'shell': [
- (r'#!/bin/bash', 5),
- (r'#!/bin/sh', 5),
- (r'\becho\s+', 1),
- (r'\$\{?\w+\}?', 1),
- ],
- 'sql': [
- (r'\bSELECT\s+', 4),
- (r'\bFROM\s+', 3),
- (r'\bWHERE\s+', 2),
- (r'\bINSERT\s+INTO', 4),
- (r'\bCREATE\s+TABLE', 4),
- ],
- 'html': [
- (r'', 1),
- ],
- }
-
- # Calculate confidence scores for each language
- scores = {}
- for lang, lang_patterns in patterns.items():
- score = 0
- for pattern, weight in lang_patterns:
- if re.search(pattern, code, re.IGNORECASE | re.MULTILINE):
- score += weight
- if score > 0:
- scores[lang] = score
-
- if not scores:
- return 'unknown', 0
-
- # Get language with highest score
- best_lang = max(scores, key=scores.get)
- confidence = min(scores[best_lang] / 10.0, 1.0) # Normalize to 0-1
-
- return best_lang, confidence
-
- def validate_code_syntax(self, code, language):
- """
- Validate code syntax (basic checks).
- Enhanced in B1.4 with syntax validation.
-
- Returns (is_valid, issues) tuple
- """
- issues = []
-
- # Common syntax checks
- if not code.strip():
- return False, ['Empty code block']
-
- # Language-specific validation
- if language == 'python':
- # Check indentation consistency
- lines = code.split('\n')
- indent_chars = set()
- for line in lines:
- if line.startswith(' '):
- indent_chars.add('space')
- elif line.startswith('\t'):
- indent_chars.add('tab')
-
- if len(indent_chars) > 1:
- issues.append('Mixed tabs and spaces')
-
- # Check for unclosed brackets/parens
- open_count = code.count('(') + code.count('[') + code.count('{')
- close_count = code.count(')') + code.count(']') + code.count('}')
- if abs(open_count - close_count) > 2: # Allow small mismatch
- issues.append('Unbalanced brackets')
-
- elif language in ['javascript', 'java', 'cpp', 'c', 'csharp', 'go']:
- # Check for balanced braces
- open_braces = code.count('{')
- close_braces = code.count('}')
- if abs(open_braces - close_braces) > 1:
- issues.append('Unbalanced braces')
-
- elif language == 'json':
- # Try to parse JSON
- try:
- json.loads(code)
- except (json.JSONDecodeError, ValueError) as e:
- issues.append(f'Invalid JSON syntax: {str(e)[:50]}')
-
- # General checks
- # Check if code looks like natural language (too many common words)
- common_words = ['the', 'and', 'for', 'with', 'this', 'that', 'have', 'from']
- word_count = sum(1 for word in common_words if word in code.lower())
- if word_count > 5 and len(code.split()) < 50:
- issues.append('May be natural language, not code')
-
- # Check code/comment ratio
- comment_lines = sum(1 for line in code.split('\n') if line.strip().startswith(('#', '//', '/*', '*', '--')))
- total_lines = len([l for l in code.split('\n') if l.strip()])
- if total_lines > 0 and comment_lines / total_lines > 0.7:
- issues.append('Mostly comments')
-
- return len(issues) == 0, issues
-
- def score_code_quality(self, code, language, confidence):
- """
- Score the quality/usefulness of detected code block.
- New in B1.4.
-
- Returns quality score (0-10)
- """
- score = 5.0 # Start with neutral score
-
- # Factor 1: Language detection confidence
- score += confidence * 2.0
-
- # Factor 2: Code length (not too short, not too long)
- code_length = len(code.strip())
- if 20 <= code_length <= 500:
- score += 1.0
- elif 500 < code_length <= 2000:
- score += 0.5
- elif code_length < 10:
- score -= 2.0
-
- # Factor 3: Number of lines
- lines = [l for l in code.split('\n') if l.strip()]
- if 2 <= len(lines) <= 50:
- score += 1.0
- elif len(lines) > 100:
- score -= 1.0
-
- # Factor 4: Has function/class definitions
- if re.search(r'\b(def|function|class|func|fn|public class)\b', code):
- score += 1.5
-
- # Factor 5: Has meaningful variable names (not just x, y, i)
- meaningful_vars = re.findall(r'\b[a-z_][a-z0-9_]{3,}\b', code.lower())
- if len(meaningful_vars) >= 2:
- score += 1.0
-
- # Factor 6: Syntax validation
- is_valid, issues = self.validate_code_syntax(code, language)
- if is_valid:
- score += 1.0
- else:
- score -= len(issues) * 0.5
-
- # Clamp score to 0-10 range
- return max(0, min(10, score))
-
- def detect_code_blocks_by_font(self, page):
- """
- Detect code blocks by analyzing font properties.
- Monospace fonts typically indicate code.
-
- Returns list of detected code blocks with metadata.
- """
- code_blocks = []
- blocks = page.get_text("dict")["blocks"]
-
- monospace_fonts = ['courier', 'mono', 'consolas', 'menlo', 'monaco', 'dejavu']
-
- current_code = []
- current_font = None
-
- for block in blocks:
- if 'lines' not in block:
- continue
-
- for line in block['lines']:
- for span in line['spans']:
- font = span['font'].lower()
- text = span['text']
-
- # Check if font is monospace
- is_monospace = any(mf in font for mf in monospace_fonts)
-
- if is_monospace:
- # Accumulate code text
- current_code.append(text)
- current_font = span['font']
- else:
- # End of code block
- if current_code:
- code_text = ''.join(current_code).strip()
- if len(code_text) > 10: # Minimum code length
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'font': current_font,
- 'detection_method': 'font'
- })
- current_code = []
- current_font = None
-
- # Handle final code block
- if current_code:
- code_text = ''.join(current_code).strip()
- if len(code_text) > 10:
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'font': current_font,
- 'detection_method': 'font'
- })
-
- return code_blocks
-
- def detect_code_blocks_by_indent(self, text):
- """
- Detect code blocks by indentation patterns.
- Code often has consistent indentation.
-
- Returns list of detected code blocks.
- """
- code_blocks = []
- lines = text.split('\n')
- current_block = []
- indent_pattern = None
-
- for line in lines:
- # Check for indentation (4 spaces or tab)
- if line.startswith(' ') or line.startswith('\t'):
- # Start or continue code block
- if not indent_pattern:
- indent_pattern = line[:4] if line.startswith(' ') else '\t'
- current_block.append(line)
- else:
- # End of code block
- if current_block and len(current_block) >= 2: # At least 2 lines
- code_text = '\n'.join(current_block).strip()
- if len(code_text) > 20: # Minimum code length
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'detection_method': 'indent'
- })
- current_block = []
- indent_pattern = None
-
- # Handle final block
- if current_block and len(current_block) >= 2:
- code_text = '\n'.join(current_block).strip()
- if len(code_text) > 20:
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'detection_method': 'indent'
- })
-
- return code_blocks
-
- def detect_code_blocks_by_pattern(self, text):
- """
- Detect code blocks by common code patterns (keywords, syntax).
-
- Returns list of detected code snippets.
- """
- code_blocks = []
-
- # Common code patterns that span multiple lines
- patterns = [
- # Function definitions
- (r'((?:def|function|func|fn|public|private)\s+\w+\s*\([^)]*\)\s*[{:]?[^}]*[}]?)', 'function'),
- # Class definitions
- (r'(class\s+\w+[^{]*\{[^}]*\})', 'class'),
- # Import statements block
- (r'((?:import|require|use|include)[^\n]+(?:\n(?:import|require|use|include)[^\n]+)*)', 'imports'),
- ]
-
- for pattern, block_type in patterns:
- matches = re.finditer(pattern, text, re.MULTILINE | re.DOTALL)
- for match in matches:
- code_text = match.group(1).strip()
- if len(code_text) > 15:
- lang, confidence = self.detect_language_from_code(code_text)
- quality = self.score_code_quality(code_text, lang, confidence)
- is_valid, issues = self.validate_code_syntax(code_text, lang)
-
- code_blocks.append({
- 'code': code_text,
- 'language': lang,
- 'confidence': confidence,
- 'quality_score': quality,
- 'is_valid': is_valid,
- 'validation_issues': issues if not is_valid else [],
- 'detection_method': 'pattern',
- 'pattern_type': block_type
- })
-
- return code_blocks
-
- def detect_chapter_start(self, page_data):
- """
- Detect if a page starts a new chapter/section.
-
- Returns (is_chapter_start, chapter_title) tuple.
- """
- headings = page_data.get('headings', [])
-
- # Check for h1 or h2 at start of page
- if headings:
- first_heading = headings[0]
- # H1 headings are strong indicators of chapters
- if first_heading['level'] in ['h1', 'h2']:
- return True, first_heading['text']
-
- # Check for specific chapter markers in text
- text = page_data.get('text', '')
- first_line = text.split('\n')[0] if text else ''
-
- chapter_patterns = [
- r'^Chapter\s+\d+',
- r'^Part\s+\d+',
- r'^Section\s+\d+',
- r'^\d+\.\s+[A-Z]', # "1. Introduction"
- ]
-
- for pattern in chapter_patterns:
- if re.match(pattern, first_line, re.IGNORECASE):
- return True, first_line.strip()
-
- return False, None
-
- def merge_continued_code_blocks(self, pages):
- """
- Merge code blocks that are split across pages.
-
- Detects when a code block at the end of one page continues
- on the next page.
- """
- for i in range(len(pages) - 1):
- current_page = pages[i]
- next_page = pages[i + 1]
-
- # Check if current page has code blocks
- if not current_page['code_samples']:
- continue
-
- # Get last code block of current page
- last_code = current_page['code_samples'][-1]
-
- # Check if next page starts with code
- if not next_page['code_samples']:
- continue
-
- first_next_code = next_page['code_samples'][0]
-
- # Same language and detection method = likely continuation
- if (last_code['language'] == first_next_code['language'] and
- last_code['detection_method'] == first_next_code['detection_method']):
-
- # Check if last code block looks incomplete (doesn't end with closing brace/etc)
- last_code_text = last_code['code'].rstrip()
- continuation_indicators = [
- not last_code_text.endswith('}'),
- not last_code_text.endswith(';'),
- last_code_text.endswith(','),
- last_code_text.endswith('\\'),
- ]
-
- if any(continuation_indicators):
- # Merge the code blocks
- merged_code = last_code['code'] + '\n' + first_next_code['code']
- last_code['code'] = merged_code
- last_code['merged_from_next_page'] = True
-
- # Remove the first code block from next page
- next_page['code_samples'].pop(0)
- next_page['code_blocks_count'] -= 1
-
- self.log(f" Merged code block from page {i+1} to {i+2}")
-
- return pages
-
- def create_chunks(self, pages):
- """
- Create chunks of pages for better organization.
-
- Returns array of chunks, each containing:
- - chunk_number
- - start_page, end_page
- - pages (array)
- - chapter_title (if detected)
- """
- if self.chunk_size == 0:
- # No chunking - return all pages as one chunk
- return [{
- 'chunk_number': 1,
- 'start_page': 1,
- 'end_page': len(pages),
- 'pages': pages,
- 'chapter_title': None
- }]
-
- chunks = []
- current_chunk = []
- chunk_start = 0
- current_chapter = None
-
- for i, page in enumerate(pages):
- # Check if this page starts a new chapter
- is_chapter, chapter_title = self.detect_chapter_start(page)
-
- if is_chapter and current_chunk:
- # Save current chunk before starting new one
- chunks.append({
- 'chunk_number': len(chunks) + 1,
- 'start_page': chunk_start + 1,
- 'end_page': i,
- 'pages': current_chunk,
- 'chapter_title': current_chapter
- })
- current_chunk = []
- chunk_start = i
- current_chapter = chapter_title
-
- if not current_chapter and is_chapter:
- current_chapter = chapter_title
-
- current_chunk.append(page)
-
- # Check if chunk size reached (but don't break chapters)
- if not is_chapter and len(current_chunk) >= self.chunk_size:
- chunks.append({
- 'chunk_number': len(chunks) + 1,
- 'start_page': chunk_start + 1,
- 'end_page': i + 1,
- 'pages': current_chunk,
- 'chapter_title': current_chapter
- })
- current_chunk = []
- chunk_start = i + 1
- current_chapter = None
-
- # Add remaining pages as final chunk
- if current_chunk:
- chunks.append({
- 'chunk_number': len(chunks) + 1,
- 'start_page': chunk_start + 1,
- 'end_page': len(pages),
- 'pages': current_chunk,
- 'chapter_title': current_chapter
- })
-
- return chunks
-
- def extract_images_from_page(self, page, page_num):
- """
- Extract images from a PDF page and save to disk (NEW in B1.5).
-
- Returns list of extracted image metadata.
- """
- if not self.extract_images:
- # Just count images, don't extract
- return []
-
- extracted = []
- image_list = page.get_images()
-
- for img_index, img in enumerate(image_list):
- try:
- xref = img[0] # Image XREF number
- base_image = self.doc.extract_image(xref)
-
- if not base_image:
- continue
-
- image_bytes = base_image["image"]
- image_ext = base_image["ext"] # png, jpeg, etc.
- width = base_image.get("width", 0)
- height = base_image.get("height", 0)
-
- # Filter out small images (icons, bullets, etc.)
- if width < self.min_image_size or height < self.min_image_size:
- self.log(f" Skipping small image: {width}x{height}")
- continue
-
- # Generate filename
- pdf_basename = Path(self.pdf_path).stem
- image_filename = f"{pdf_basename}_page{page_num+1}_img{img_index+1}.{image_ext}"
-
- # Save image
- image_path = Path(self.image_dir) / image_filename
- image_path.parent.mkdir(parents=True, exist_ok=True)
-
- with open(image_path, "wb") as f:
- f.write(image_bytes)
-
- # Store metadata
- image_info = {
- 'filename': image_filename,
- 'path': str(image_path),
- 'page_number': page_num + 1,
- 'width': width,
- 'height': height,
- 'format': image_ext,
- 'size_bytes': len(image_bytes),
- 'xref': xref
- }
-
- extracted.append(image_info)
- self.extracted_images.append(image_info)
- self.log(f" Extracted image: {image_filename} ({width}x{height})")
-
- except Exception as e:
- self.log(f" Error extracting image {img_index}: {e}")
- continue
-
- return extracted
-
- def extract_page(self, page_num):
- """
- Extract content from a single PDF page.
-
- Returns dict with page content, code blocks, and metadata.
- """
- # Check cache first (Priority 3)
- cache_key = f"page_{page_num}"
- cached = self.get_cached(cache_key)
- if cached is not None:
- self.log(f" Page {page_num + 1}: Using cached data")
- return cached
-
- page = self.doc.load_page(page_num)
-
- # Extract plain text (with OCR if enabled - Priority 2)
- if self.use_ocr:
- text = self.extract_text_with_ocr(page)
- else:
- text = page.get_text("text")
-
- # Extract markdown (better structure preservation)
- markdown = page.get_text("markdown")
-
- # Extract tables (Priority 2)
- tables = self.extract_tables_from_page(page)
-
- # Get page images (for diagrams)
- images = page.get_images()
-
- # Extract images to files (NEW in B1.5)
- extracted_images = self.extract_images_from_page(page, page_num)
-
- # Detect code blocks using multiple methods
- font_code_blocks = self.detect_code_blocks_by_font(page)
- indent_code_blocks = self.detect_code_blocks_by_indent(text)
- pattern_code_blocks = self.detect_code_blocks_by_pattern(text)
-
- # Merge and deduplicate code blocks
- all_code_blocks = font_code_blocks + indent_code_blocks + pattern_code_blocks
-
- # Simple deduplication by code content
- unique_code = {}
- for block in all_code_blocks:
- code_hash = hash(block['code'])
- if code_hash not in unique_code:
- unique_code[code_hash] = block
- else:
- # Keep the one with higher quality score
- if block['quality_score'] > unique_code[code_hash]['quality_score']:
- unique_code[code_hash] = block
-
- code_samples = list(unique_code.values())
-
- # Filter by minimum quality (NEW in B1.4)
- if self.min_quality > 0:
- code_samples_before = len(code_samples)
- code_samples = [c for c in code_samples if c['quality_score'] >= self.min_quality]
- filtered_count = code_samples_before - len(code_samples)
- if filtered_count > 0:
- self.log(f" Filtered out {filtered_count} low-quality code blocks (min_quality={self.min_quality})")
-
- # Sort by quality score (highest first)
- code_samples.sort(key=lambda x: x['quality_score'], reverse=True)
-
- # Extract headings from markdown
- headings = []
- for line in markdown.split('\n'):
- if line.startswith('#'):
- level = len(line) - len(line.lstrip('#'))
- text = line.lstrip('#').strip()
- if text:
- headings.append({
- 'level': f'h{level}',
- 'text': text
- })
-
- page_data = {
- 'page_number': page_num + 1, # 1-indexed for humans
- 'text': text.strip(),
- 'markdown': markdown.strip(),
- 'headings': headings,
- 'code_samples': code_samples,
- 'images_count': len(images),
- 'extracted_images': extracted_images, # NEW in B1.5
- 'tables': tables, # NEW in Priority 2
- 'char_count': len(text),
- 'code_blocks_count': len(code_samples),
- 'tables_count': len(tables) # NEW in Priority 2
- }
-
- # Cache the result (Priority 3)
- self.set_cached(cache_key, page_data)
-
- self.log(f" Page {page_num + 1}: {len(text)} chars, {len(code_samples)} code blocks, {len(headings)} headings, {len(extracted_images)} images, {len(tables)} tables")
-
- return page_data
-
- def extract_all(self):
- """
- Extract content from all pages of the PDF.
- Enhanced with password support and parallel processing.
-
- Returns dict with metadata and pages array.
- """
- print(f"\n📄 Extracting from: {self.pdf_path}")
-
- # Open PDF (with password support - Priority 2)
- try:
- self.doc = fitz.open(self.pdf_path)
-
- # Handle encrypted PDFs (Priority 2)
- if self.doc.is_encrypted:
- if self.password:
- print(f" 🔐 PDF is encrypted, trying password...")
- if self.doc.authenticate(self.password):
- print(f" ✅ Password accepted")
- else:
- print(f" ❌ Invalid password")
- return None
- else:
- print(f" ❌ PDF is encrypted but no password provided")
- print(f" Use --password option to provide password")
- return None
-
- except Exception as e:
- print(f"❌ Error opening PDF: {e}")
- return None
-
- print(f" Pages: {len(self.doc)}")
- print(f" Metadata: {self.doc.metadata}")
-
- # Set up image directory (NEW in B1.5)
- if self.extract_images and not self.image_dir:
- pdf_basename = Path(self.pdf_path).stem
- self.image_dir = f"output/{pdf_basename}_images"
- print(f" Image directory: {self.image_dir}")
-
- # Show feature status
- if self.use_ocr:
- status = "✅ enabled" if TESSERACT_AVAILABLE else "⚠️ not available (install pytesseract)"
- print(f" OCR: {status}")
- if self.extract_tables:
- print(f" Table extraction: ✅ enabled")
- if self.parallel:
- status = "✅ enabled" if CONCURRENT_AVAILABLE else "⚠️ not available"
- print(f" Parallel processing: {status} ({self.max_workers} workers)")
- if self.use_cache:
- print(f" Caching: ✅ enabled")
-
- print("")
-
- # Extract each page (with parallel processing - Priority 3)
- if self.parallel and CONCURRENT_AVAILABLE and len(self.doc) > 5:
- print(f"🚀 Extracting {len(self.doc)} pages in parallel ({self.max_workers} workers)...")
- with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
- page_numbers = list(range(len(self.doc)))
- self.pages = list(executor.map(self.extract_page, page_numbers))
- else:
- # Sequential extraction
- for page_num in range(len(self.doc)):
- page_data = self.extract_page(page_num)
- self.pages.append(page_data)
-
- # Merge code blocks that span across pages
- self.log("\n🔗 Merging code blocks across pages...")
- self.pages = self.merge_continued_code_blocks(self.pages)
-
- # Create chunks
- self.log(f"\n📦 Creating chunks (chunk_size={self.chunk_size})...")
- chunks = self.create_chunks(self.pages)
-
- # Build summary
- total_chars = sum(p['char_count'] for p in self.pages)
- total_code_blocks = sum(p['code_blocks_count'] for p in self.pages)
- total_headings = sum(len(p['headings']) for p in self.pages)
- total_images = sum(p['images_count'] for p in self.pages)
- total_tables = sum(p['tables_count'] for p in self.pages) # NEW in Priority 2
-
- # Detect languages used
- languages = {}
- all_code_blocks_list = []
- for page in self.pages:
- for code in page['code_samples']:
- lang = code['language']
- languages[lang] = languages.get(lang, 0) + 1
- all_code_blocks_list.append(code)
-
- # Calculate quality statistics (NEW in B1.4)
- quality_stats = {}
- if all_code_blocks_list:
- quality_scores = [c['quality_score'] for c in all_code_blocks_list]
- confidences = [c['confidence'] for c in all_code_blocks_list]
- valid_count = sum(1 for c in all_code_blocks_list if c['is_valid'])
-
- quality_stats = {
- 'average_quality': sum(quality_scores) / len(quality_scores),
- 'average_confidence': sum(confidences) / len(confidences),
- 'valid_code_blocks': valid_count,
- 'invalid_code_blocks': total_code_blocks - valid_count,
- 'validation_rate': valid_count / total_code_blocks if total_code_blocks > 0 else 0,
- 'high_quality_blocks': sum(1 for s in quality_scores if s >= 7.0),
- 'medium_quality_blocks': sum(1 for s in quality_scores if 4.0 <= s < 7.0),
- 'low_quality_blocks': sum(1 for s in quality_scores if s < 4.0),
- }
-
- # Extract chapter information
- chapters = []
- for chunk in chunks:
- if chunk['chapter_title']:
- chapters.append({
- 'title': chunk['chapter_title'],
- 'start_page': chunk['start_page'],
- 'end_page': chunk['end_page']
- })
-
- result = {
- 'source_file': self.pdf_path,
- 'metadata': self.doc.metadata,
- 'total_pages': len(self.doc),
- 'total_chars': total_chars,
- 'total_code_blocks': total_code_blocks,
- 'total_headings': total_headings,
- 'total_images': total_images,
- 'total_extracted_images': len(self.extracted_images), # NEW in B1.5
- 'total_tables': total_tables, # NEW in Priority 2
- 'image_directory': self.image_dir if self.extract_images else None, # NEW in B1.5
- 'extracted_images': self.extracted_images, # NEW in B1.5
- 'total_chunks': len(chunks),
- 'chapters': chapters,
- 'languages_detected': languages,
- 'quality_statistics': quality_stats, # NEW in B1.4
- 'chunks': chunks,
- 'pages': self.pages # Still include all pages for compatibility
- }
-
- # Close document
- self.doc.close()
-
- print(f"\n✅ Extraction complete:")
- print(f" Total characters: {total_chars:,}")
- print(f" Code blocks found: {total_code_blocks}")
- print(f" Headings found: {total_headings}")
- print(f" Images found: {total_images}")
- if self.extract_images:
- print(f" Images extracted: {len(self.extracted_images)}")
- if self.image_dir:
- print(f" Image directory: {self.image_dir}")
- if self.extract_tables:
- print(f" Tables found: {total_tables}")
- print(f" Chunks created: {len(chunks)}")
- print(f" Chapters detected: {len(chapters)}")
- print(f" Languages detected: {', '.join(languages.keys())}")
-
- # Print quality statistics (NEW in B1.4)
- if quality_stats:
- print(f"\n📊 Code Quality Statistics:")
- print(f" Average quality: {quality_stats['average_quality']:.1f}/10")
- print(f" Average confidence: {quality_stats['average_confidence']:.1%}")
- print(f" Valid code blocks: {quality_stats['valid_code_blocks']}/{total_code_blocks} ({quality_stats['validation_rate']:.1%})")
- print(f" High quality (7+): {quality_stats['high_quality_blocks']}")
- print(f" Medium quality (4-7): {quality_stats['medium_quality_blocks']}")
- print(f" Low quality (<4): {quality_stats['low_quality_blocks']}")
-
- return result
-
-
-def main():
- parser = argparse.ArgumentParser(
- description='Extract text and code blocks from PDF documentation',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Extract from PDF
- python3 pdf_extractor_poc.py input.pdf
-
- # Save to JSON file
- python3 pdf_extractor_poc.py input.pdf --output result.json
-
- # Verbose mode
- python3 pdf_extractor_poc.py input.pdf --verbose
-
- # Extract and save
- python3 pdf_extractor_poc.py docs/python.pdf -o python_extracted.json -v
- """
- )
-
- parser.add_argument('pdf_file', help='Path to PDF file to extract')
- parser.add_argument('-o', '--output', help='Output JSON file path (default: print to stdout)')
- parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
- parser.add_argument('--pretty', action='store_true', help='Pretty-print JSON output')
- parser.add_argument('--chunk-size', type=int, default=10,
- help='Pages per chunk (0 = no chunking, default: 10)')
- parser.add_argument('--no-merge', action='store_true',
- help='Disable merging code blocks across pages')
- parser.add_argument('--min-quality', type=float, default=0.0,
- help='Minimum code quality score (0-10, default: 0 = no filtering)')
- parser.add_argument('--extract-images', action='store_true',
- help='Extract images to files (NEW in B1.5)')
- parser.add_argument('--image-dir', type=str, default=None,
- help='Directory to save extracted images (default: output/{pdf_name}_images)')
- parser.add_argument('--min-image-size', type=int, default=100,
- help='Minimum image dimension in pixels (filters icons, default: 100)')
-
- # Advanced features (Priority 2 & 3)
- parser.add_argument('--ocr', action='store_true',
- help='Use OCR for scanned PDFs (requires pytesseract)')
- parser.add_argument('--password', type=str, default=None,
- help='Password for encrypted PDF')
- parser.add_argument('--extract-tables', action='store_true',
- help='Extract tables from PDF (Priority 2)')
- parser.add_argument('--parallel', action='store_true',
- help='Process pages in parallel (Priority 3)')
- parser.add_argument('--workers', type=int, default=None,
- help='Number of parallel workers (default: CPU count)')
- parser.add_argument('--no-cache', action='store_true',
- help='Disable caching of expensive operations')
-
- args = parser.parse_args()
-
- # Validate input file
- if not os.path.exists(args.pdf_file):
- print(f"❌ Error: File not found: {args.pdf_file}")
- sys.exit(1)
-
- if not args.pdf_file.lower().endswith('.pdf'):
- print(f"⚠️ Warning: File does not have .pdf extension")
-
- # Extract
- extractor = PDFExtractor(
- args.pdf_file,
- verbose=args.verbose,
- chunk_size=args.chunk_size,
- min_quality=args.min_quality,
- extract_images=args.extract_images,
- image_dir=args.image_dir,
- min_image_size=args.min_image_size,
- # Advanced features (Priority 2 & 3)
- use_ocr=args.ocr,
- password=args.password,
- extract_tables=args.extract_tables,
- parallel=args.parallel,
- max_workers=args.workers,
- use_cache=not args.no_cache
- )
- result = extractor.extract_all()
-
- if result is None:
- sys.exit(1)
-
- # Output
- if args.output:
- # Save to file
- with open(args.output, 'w', encoding='utf-8') as f:
- if args.pretty:
- json.dump(result, f, indent=2, ensure_ascii=False)
- else:
- json.dump(result, f, ensure_ascii=False)
- print(f"\n💾 Saved to: {args.output}")
- else:
- # Print to stdout
- if args.pretty:
- print("\n" + json.dumps(result, indent=2, ensure_ascii=False))
- else:
- print(json.dumps(result, ensure_ascii=False))
-
-
-if __name__ == '__main__':
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/pdf_scraper.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/pdf_scraper.py
deleted file mode 100644
index 76ce377..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/pdf_scraper.py
+++ /dev/null
@@ -1,401 +0,0 @@
-#!/usr/bin/env python3
-"""
-PDF Documentation to Claude Skill Converter (Task B1.6)
-
-Converts PDF documentation into Claude AI skills.
-Uses pdf_extractor_poc.py for extraction, builds skill structure.
-
-Usage:
- python3 pdf_scraper.py --config configs/manual_pdf.json
- python3 pdf_scraper.py --pdf manual.pdf --name myskill
- python3 pdf_scraper.py --from-json manual_extracted.json
-"""
-
-import os
-import sys
-import json
-import re
-import argparse
-from pathlib import Path
-
-# Import the PDF extractor
-from .pdf_extractor_poc import PDFExtractor
-
-
-class PDFToSkillConverter:
- """Convert PDF documentation to Claude skill"""
-
- def __init__(self, config):
- self.config = config
- self.name = config['name']
- self.pdf_path = config.get('pdf_path', '')
- self.description = config.get('description', f'Documentation skill for {self.name}')
-
- # Paths
- self.skill_dir = f"output/{self.name}"
- self.data_file = f"output/{self.name}_extracted.json"
-
- # Extraction options
- self.extract_options = config.get('extract_options', {})
-
- # Categories
- self.categories = config.get('categories', {})
-
- # Extracted data
- self.extracted_data = None
-
- def extract_pdf(self):
- """Extract content from PDF using pdf_extractor_poc.py"""
- print(f"\n🔍 Extracting from PDF: {self.pdf_path}")
-
- # Create extractor with options
- extractor = PDFExtractor(
- self.pdf_path,
- verbose=True,
- chunk_size=self.extract_options.get('chunk_size', 10),
- min_quality=self.extract_options.get('min_quality', 5.0),
- extract_images=self.extract_options.get('extract_images', True),
- image_dir=f"{self.skill_dir}/assets/images",
- min_image_size=self.extract_options.get('min_image_size', 100)
- )
-
- # Extract
- result = extractor.extract_all()
-
- if not result:
- print("❌ Extraction failed")
- raise RuntimeError(f"Failed to extract PDF: {self.pdf_path}")
-
- # Save extracted data
- with open(self.data_file, 'w', encoding='utf-8') as f:
- json.dump(result, f, indent=2, ensure_ascii=False)
-
- print(f"\n💾 Saved extracted data to: {self.data_file}")
- self.extracted_data = result
- return True
-
- def load_extracted_data(self, json_path):
- """Load previously extracted data from JSON"""
- print(f"\n📂 Loading extracted data from: {json_path}")
-
- with open(json_path, 'r', encoding='utf-8') as f:
- self.extracted_data = json.load(f)
-
- print(f"✅ Loaded {self.extracted_data['total_pages']} pages")
- return True
-
- def categorize_content(self):
- """Categorize pages based on chapters or keywords"""
- print(f"\n📋 Categorizing content...")
-
- categorized = {}
-
- # Use chapters if available
- if self.extracted_data.get('chapters'):
- for chapter in self.extracted_data['chapters']:
- category_key = self._sanitize_filename(chapter['title'])
- categorized[category_key] = {
- 'title': chapter['title'],
- 'pages': []
- }
-
- # Assign pages to chapters
- for page in self.extracted_data['pages']:
- page_num = page['page_number']
-
- # Find which chapter this page belongs to
- for chapter in self.extracted_data['chapters']:
- if chapter['start_page'] <= page_num <= chapter['end_page']:
- category_key = self._sanitize_filename(chapter['title'])
- categorized[category_key]['pages'].append(page)
- break
-
- # Fall back to keyword-based categorization
- elif self.categories:
- # Check if categories is already in the right format (for tests)
- # If first value is a list of dicts (pages), use as-is
- first_value = next(iter(self.categories.values()))
- if isinstance(first_value, list) and first_value and isinstance(first_value[0], dict):
- # Already categorized - convert to expected format
- for cat_key, pages in self.categories.items():
- categorized[cat_key] = {
- 'title': cat_key.replace('_', ' ').title(),
- 'pages': pages
- }
- else:
- # Keyword-based categorization
- # Initialize categories
- for cat_key, keywords in self.categories.items():
- categorized[cat_key] = {
- 'title': cat_key.replace('_', ' ').title(),
- 'pages': []
- }
-
- # Categorize by keywords
- for page in self.extracted_data['pages']:
- text = page.get('text', '').lower()
- headings_text = ' '.join([h['text'] for h in page.get('headings', [])]).lower()
-
- # Score against each category
- scores = {}
- for cat_key, keywords in self.categories.items():
- # Handle both string keywords and dict keywords (shouldn't happen, but be safe)
- if isinstance(keywords, list):
- score = sum(1 for kw in keywords
- if isinstance(kw, str) and (kw.lower() in text or kw.lower() in headings_text))
- else:
- score = 0
- if score > 0:
- scores[cat_key] = score
-
- # Assign to highest scoring category
- if scores:
- best_cat = max(scores, key=scores.get)
- categorized[best_cat]['pages'].append(page)
- else:
- # Default category
- if 'other' not in categorized:
- categorized['other'] = {'title': 'Other', 'pages': []}
- categorized['other']['pages'].append(page)
-
- else:
- # No categorization - use single category
- categorized['content'] = {
- 'title': 'Content',
- 'pages': self.extracted_data['pages']
- }
-
- print(f"✅ Created {len(categorized)} categories")
- for cat_key, cat_data in categorized.items():
- print(f" - {cat_data['title']}: {len(cat_data['pages'])} pages")
-
- return categorized
-
- def build_skill(self):
- """Build complete skill structure"""
- print(f"\n🏗️ Building skill: {self.name}")
-
- # Create directories
- os.makedirs(f"{self.skill_dir}/references", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/scripts", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/assets", exist_ok=True)
-
- # Categorize content
- categorized = self.categorize_content()
-
- # Generate reference files
- print(f"\n📝 Generating reference files...")
- for cat_key, cat_data in categorized.items():
- self._generate_reference_file(cat_key, cat_data)
-
- # Generate index
- self._generate_index(categorized)
-
- # Generate SKILL.md
- self._generate_skill_md(categorized)
-
- print(f"\n✅ Skill built successfully: {self.skill_dir}/")
- print(f"\n📦 Next step: Package with: skill-seekers package {self.skill_dir}/")
-
- def _generate_reference_file(self, cat_key, cat_data):
- """Generate a reference markdown file for a category"""
- filename = f"{self.skill_dir}/references/{cat_key}.md"
-
- with open(filename, 'w', encoding='utf-8') as f:
- f.write(f"# {cat_data['title']}\n\n")
-
- for page in cat_data['pages']:
- # Add headings as section markers
- if page.get('headings'):
- f.write(f"## {page['headings'][0]['text']}\n\n")
-
- # Add text content
- if page.get('text'):
- # Limit to first 1000 chars per page to avoid huge files
- text = page['text'][:1000]
- f.write(f"{text}\n\n")
-
- # Add code samples (check both 'code_samples' and 'code_blocks' for compatibility)
- code_list = page.get('code_samples') or page.get('code_blocks')
- if code_list:
- f.write("### Code Examples\n\n")
- for code in code_list[:3]: # Limit to top 3
- lang = code.get('language', '')
- f.write(f"```{lang}\n{code['code']}\n```\n\n")
-
- # Add images
- if page.get('images'):
- # Create assets directory if needed
- assets_dir = os.path.join(self.skill_dir, 'assets')
- os.makedirs(assets_dir, exist_ok=True)
-
- f.write("### Images\n\n")
- for img in page['images']:
- # Save image to assets
- img_filename = f"page_{page['page_number']}_img_{img['index']}.png"
- img_path = os.path.join(assets_dir, img_filename)
-
- with open(img_path, 'wb') as img_file:
- img_file.write(img['data'])
-
- # Add markdown image reference
- f.write(f"![Image {img['index']}](../assets/{img_filename})\n\n")
-
- f.write("---\n\n")
-
- print(f" Generated: {filename}")
-
- def _generate_index(self, categorized):
- """Generate reference index"""
- filename = f"{self.skill_dir}/references/index.md"
-
- with open(filename, 'w', encoding='utf-8') as f:
- f.write(f"# {self.name.title()} Documentation Reference\n\n")
- f.write("## Categories\n\n")
-
- for cat_key, cat_data in categorized.items():
- page_count = len(cat_data['pages'])
- f.write(f"- [{cat_data['title']}]({cat_key}.md) ({page_count} pages)\n")
-
- f.write("\n## Statistics\n\n")
- stats = self.extracted_data.get('quality_statistics', {})
- f.write(f"- Total pages: {self.extracted_data.get('total_pages', 0)}\n")
- f.write(f"- Code blocks: {self.extracted_data.get('total_code_blocks', 0)}\n")
- f.write(f"- Images: {self.extracted_data.get('total_images', 0)}\n")
- if stats:
- f.write(f"- Average code quality: {stats.get('average_quality', 0):.1f}/10\n")
- f.write(f"- Valid code blocks: {stats.get('valid_code_blocks', 0)}\n")
-
- print(f" Generated: {filename}")
-
- def _generate_skill_md(self, categorized):
- """Generate main SKILL.md file"""
- filename = f"{self.skill_dir}/SKILL.md"
-
- # Generate skill name (lowercase, hyphens only, max 64 chars)
- skill_name = self.name.lower().replace('_', '-').replace(' ', '-')[:64]
-
- # Truncate description to 1024 chars if needed
- desc = self.description[:1024] if len(self.description) > 1024 else self.description
-
- with open(filename, 'w', encoding='utf-8') as f:
- # Write YAML frontmatter
- f.write(f"---\n")
- f.write(f"name: {skill_name}\n")
- f.write(f"description: {desc}\n")
- f.write(f"---\n\n")
-
- f.write(f"# {self.name.title()} Documentation Skill\n\n")
- f.write(f"{self.description}\n\n")
-
- f.write("## When to use this skill\n\n")
- f.write(f"Use this skill when the user asks about {self.name} documentation, ")
- f.write("including API references, tutorials, examples, and best practices.\n\n")
-
- f.write("## What's included\n\n")
- f.write("This skill contains:\n\n")
- for cat_key, cat_data in categorized.items():
- f.write(f"- **{cat_data['title']}**: {len(cat_data['pages'])} pages\n")
-
- f.write("\n## Quick Reference\n\n")
-
- # Get high-quality code samples
- all_code = []
- for page in self.extracted_data['pages']:
- all_code.extend(page.get('code_samples', []))
-
- # Sort by quality and get top 5
- all_code.sort(key=lambda x: x.get('quality_score', 0), reverse=True)
- top_code = all_code[:5]
-
- if top_code:
- f.write("### Top Code Examples\n\n")
- for i, code in enumerate(top_code, 1):
- lang = code['language']
- quality = code.get('quality_score', 0)
- f.write(f"**Example {i}** (Quality: {quality:.1f}/10):\n\n")
- f.write(f"```{lang}\n{code['code'][:300]}...\n```\n\n")
-
- f.write("## Navigation\n\n")
- f.write("See `references/index.md` for complete documentation structure.\n\n")
-
- # Add language statistics
- langs = self.extracted_data.get('languages_detected', {})
- if langs:
- f.write("## Languages Covered\n\n")
- for lang, count in sorted(langs.items(), key=lambda x: x[1], reverse=True):
- f.write(f"- {lang}: {count} examples\n")
-
- print(f" Generated: {filename}")
-
- def _sanitize_filename(self, name):
- """Convert string to safe filename"""
- # Remove special chars, replace spaces with underscores
- safe = re.sub(r'[^\w\s-]', '', name.lower())
- safe = re.sub(r'[-\s]+', '_', safe)
- return safe
-
-
-def main():
- parser = argparse.ArgumentParser(
- description='Convert PDF documentation to Claude skill',
- formatter_class=argparse.RawDescriptionHelpFormatter
- )
-
- parser.add_argument('--config', help='PDF config JSON file')
- parser.add_argument('--pdf', help='Direct PDF file path')
- parser.add_argument('--name', help='Skill name (with --pdf)')
- parser.add_argument('--from-json', help='Build skill from extracted JSON')
- parser.add_argument('--description', help='Skill description')
-
- args = parser.parse_args()
-
- # Validate inputs
- if not (args.config or args.pdf or args.from_json):
- parser.error("Must specify --config, --pdf, or --from-json")
-
- # Load or create config
- if args.config:
- with open(args.config, 'r') as f:
- config = json.load(f)
- elif args.from_json:
- # Build from extracted JSON
- name = Path(args.from_json).stem.replace('_extracted', '')
- config = {
- 'name': name,
- 'description': args.description or f'Documentation skill for {name}'
- }
- converter = PDFToSkillConverter(config)
- converter.load_extracted_data(args.from_json)
- converter.build_skill()
- return
- else:
- # Direct PDF mode
- if not args.name:
- parser.error("Must specify --name with --pdf")
- config = {
- 'name': args.name,
- 'pdf_path': args.pdf,
- 'description': args.description or f'Documentation skill for {args.name}',
- 'extract_options': {
- 'chunk_size': 10,
- 'min_quality': 5.0,
- 'extract_images': True,
- 'min_image_size': 100
- }
- }
-
- # Create converter
- converter = PDFToSkillConverter(config)
-
- # Extract if needed
- if config.get('pdf_path'):
- if not converter.extract_pdf():
- sys.exit(1)
-
- # Build skill
- converter.build_skill()
-
-
-if __name__ == '__main__':
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/quality_checker.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/quality_checker.py
deleted file mode 100644
index 8ff66c5..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/quality_checker.py
+++ /dev/null
@@ -1,480 +0,0 @@
-#!/usr/bin/env python3
-"""
-Quality Checker for Claude Skills
-Validates skill quality, checks links, and generates quality reports.
-
-Usage:
- python3 quality_checker.py output/react/
- python3 quality_checker.py output/godot/ --verbose
-"""
-
-import os
-import re
-import sys
-from pathlib import Path
-from typing import Dict, List, Tuple, Optional
-from dataclasses import dataclass, field
-
-
-@dataclass
-class QualityIssue:
- """Represents a quality issue found during validation."""
- level: str # 'error', 'warning', 'info'
- category: str # 'enhancement', 'content', 'links', 'structure'
- message: str
- file: Optional[str] = None
- line: Optional[int] = None
-
-
-@dataclass
-class QualityReport:
- """Complete quality report for a skill."""
- skill_name: str
- skill_path: Path
- errors: List[QualityIssue] = field(default_factory=list)
- warnings: List[QualityIssue] = field(default_factory=list)
- info: List[QualityIssue] = field(default_factory=list)
-
- def add_error(self, category: str, message: str, file: str = None, line: int = None):
- """Add an error to the report."""
- self.errors.append(QualityIssue('error', category, message, file, line))
-
- def add_warning(self, category: str, message: str, file: str = None, line: int = None):
- """Add a warning to the report."""
- self.warnings.append(QualityIssue('warning', category, message, file, line))
-
- def add_info(self, category: str, message: str, file: str = None, line: int = None):
- """Add info to the report."""
- self.info.append(QualityIssue('info', category, message, file, line))
-
- @property
- def has_errors(self) -> bool:
- """Check if there are any errors."""
- return len(self.errors) > 0
-
- @property
- def has_warnings(self) -> bool:
- """Check if there are any warnings."""
- return len(self.warnings) > 0
-
- @property
- def is_excellent(self) -> bool:
- """Check if quality is excellent (no errors, no warnings)."""
- return not self.has_errors and not self.has_warnings
-
- @property
- def quality_score(self) -> float:
- """Calculate quality score (0-100)."""
- # Start with perfect score
- score = 100.0
-
- # Deduct points for issues
- score -= len(self.errors) * 15 # -15 per error
- score -= len(self.warnings) * 5 # -5 per warning
-
- # Never go below 0
- return max(0.0, score)
-
- @property
- def quality_grade(self) -> str:
- """Get quality grade (A-F)."""
- score = self.quality_score
- if score >= 90:
- return 'A'
- elif score >= 80:
- return 'B'
- elif score >= 70:
- return 'C'
- elif score >= 60:
- return 'D'
- else:
- return 'F'
-
-
-class SkillQualityChecker:
- """Validates skill quality and generates reports."""
-
- def __init__(self, skill_dir: Path):
- """Initialize quality checker.
-
- Args:
- skill_dir: Path to skill directory
- """
- self.skill_dir = Path(skill_dir)
- self.skill_md_path = self.skill_dir / "SKILL.md"
- self.references_dir = self.skill_dir / "references"
- self.report = QualityReport(
- skill_name=self.skill_dir.name,
- skill_path=self.skill_dir
- )
-
- def check_all(self) -> QualityReport:
- """Run all quality checks and return report.
-
- Returns:
- QualityReport: Complete quality report
- """
- # Basic structure checks
- self._check_skill_structure()
-
- # Enhancement verification
- self._check_enhancement_quality()
-
- # Content quality checks
- self._check_content_quality()
-
- # Link validation
- self._check_links()
-
- return self.report
-
- def _check_skill_structure(self):
- """Check basic skill structure."""
- # Check SKILL.md exists
- if not self.skill_md_path.exists():
- self.report.add_error(
- 'structure',
- 'SKILL.md file not found',
- str(self.skill_md_path)
- )
- return
-
- # Check references directory exists
- if not self.references_dir.exists():
- self.report.add_warning(
- 'structure',
- 'references/ directory not found - skill may be incomplete',
- str(self.references_dir)
- )
- elif not list(self.references_dir.glob('*.md')):
- self.report.add_warning(
- 'structure',
- 'references/ directory is empty - no reference documentation found',
- str(self.references_dir)
- )
-
- def _check_enhancement_quality(self):
- """Check if SKILL.md was properly enhanced."""
- if not self.skill_md_path.exists():
- return
-
- content = self.skill_md_path.read_text(encoding='utf-8')
-
- # Check for template indicators (signs it wasn't enhanced)
- template_indicators = [
- "TODO:",
- "[Add description]",
- "[Framework specific tips]",
- "coming soon",
- ]
-
- for indicator in template_indicators:
- if indicator.lower() in content.lower():
- self.report.add_warning(
- 'enhancement',
- f'Found template placeholder: "{indicator}" - SKILL.md may not be enhanced',
- 'SKILL.md'
- )
-
- # Check for good signs of enhancement
- enhancement_indicators = {
- 'code_examples': re.compile(r'```[\w-]+\n', re.MULTILINE),
- 'real_examples': re.compile(r'Example:', re.IGNORECASE),
- 'sections': re.compile(r'^## .+', re.MULTILINE),
- }
-
- code_blocks = len(enhancement_indicators['code_examples'].findall(content))
- real_examples = len(enhancement_indicators['real_examples'].findall(content))
- sections = len(enhancement_indicators['sections'].findall(content))
-
- # Quality thresholds
- if code_blocks == 0:
- self.report.add_warning(
- 'enhancement',
- 'No code examples found in SKILL.md - consider enhancing',
- 'SKILL.md'
- )
- elif code_blocks < 3:
- self.report.add_info(
- 'enhancement',
- f'Only {code_blocks} code examples found - more examples would improve quality',
- 'SKILL.md'
- )
- else:
- self.report.add_info(
- 'enhancement',
- f'✓ Found {code_blocks} code examples',
- 'SKILL.md'
- )
-
- if sections < 4:
- self.report.add_warning(
- 'enhancement',
- f'Only {sections} sections found - SKILL.md may be too basic',
- 'SKILL.md'
- )
- else:
- self.report.add_info(
- 'enhancement',
- f'✓ Found {sections} sections',
- 'SKILL.md'
- )
-
- def _check_content_quality(self):
- """Check content quality."""
- if not self.skill_md_path.exists():
- return
-
- content = self.skill_md_path.read_text(encoding='utf-8')
-
- # Check YAML frontmatter
- if not content.startswith('---'):
- self.report.add_error(
- 'content',
- 'Missing YAML frontmatter - SKILL.md must start with ---',
- 'SKILL.md',
- 1
- )
- else:
- # Extract frontmatter
- try:
- frontmatter_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
- if frontmatter_match:
- frontmatter = frontmatter_match.group(1)
-
- # Check for required fields
- if 'name:' not in frontmatter:
- self.report.add_error(
- 'content',
- 'Missing "name:" field in YAML frontmatter',
- 'SKILL.md',
- 2
- )
-
- # Check for description
- if 'description:' in frontmatter:
- self.report.add_info(
- 'content',
- '✓ YAML frontmatter includes description',
- 'SKILL.md'
- )
- else:
- self.report.add_error(
- 'content',
- 'Invalid YAML frontmatter format',
- 'SKILL.md',
- 1
- )
- except Exception as e:
- self.report.add_error(
- 'content',
- f'Error parsing YAML frontmatter: {e}',
- 'SKILL.md',
- 1
- )
-
- # Check code block language tags
- code_blocks_without_lang = re.findall(r'```\n[^`]', content)
- if code_blocks_without_lang:
- self.report.add_warning(
- 'content',
- f'Found {len(code_blocks_without_lang)} code blocks without language tags',
- 'SKILL.md'
- )
-
- # Check for "When to Use" section
- if 'when to use' not in content.lower():
- self.report.add_warning(
- 'content',
- 'Missing "When to Use This Skill" section',
- 'SKILL.md'
- )
- else:
- self.report.add_info(
- 'content',
- '✓ Found "When to Use" section',
- 'SKILL.md'
- )
-
- # Check reference files
- if self.references_dir.exists():
- ref_files = list(self.references_dir.glob('*.md'))
- if ref_files:
- self.report.add_info(
- 'content',
- f'✓ Found {len(ref_files)} reference files',
- 'references/'
- )
-
- # Check if references are mentioned in SKILL.md
- mentioned_refs = 0
- for ref_file in ref_files:
- if ref_file.name in content:
- mentioned_refs += 1
-
- if mentioned_refs == 0:
- self.report.add_warning(
- 'content',
- 'Reference files exist but none are mentioned in SKILL.md',
- 'SKILL.md'
- )
-
- def _check_links(self):
- """Check internal markdown links."""
- if not self.skill_md_path.exists():
- return
-
- content = self.skill_md_path.read_text(encoding='utf-8')
-
- # Find all markdown links [text](path)
- link_pattern = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
- links = link_pattern.findall(content)
-
- broken_links = []
-
- for text, link in links:
- # Skip external links (http/https)
- if link.startswith('http://') or link.startswith('https://'):
- continue
-
- # Skip anchor links
- if link.startswith('#'):
- continue
-
- # Check if file exists (relative to SKILL.md)
- link_path = self.skill_dir / link
- if not link_path.exists():
- broken_links.append((text, link))
-
- if broken_links:
- for text, link in broken_links:
- self.report.add_warning(
- 'links',
- f'Broken link: [{text}]({link})',
- 'SKILL.md'
- )
- else:
- if links:
- internal_links = [l for t, l in links if not l.startswith('http')]
- if internal_links:
- self.report.add_info(
- 'links',
- f'✓ All {len(internal_links)} internal links are valid',
- 'SKILL.md'
- )
-
-
-def print_report(report: QualityReport, verbose: bool = False):
- """Print quality report to console.
-
- Args:
- report: Quality report to print
- verbose: Show all info messages
- """
- print("\n" + "=" * 60)
- print(f"QUALITY REPORT: {report.skill_name}")
- print("=" * 60)
- print()
-
- # Quality score
- print(f"Quality Score: {report.quality_score:.1f}/100 (Grade: {report.quality_grade})")
- print()
-
- # Errors
- if report.errors:
- print(f"❌ ERRORS ({len(report.errors)}):")
- for issue in report.errors:
- location = f" ({issue.file}:{issue.line})" if issue.file and issue.line else f" ({issue.file})" if issue.file else ""
- print(f" [{issue.category}] {issue.message}{location}")
- print()
-
- # Warnings
- if report.warnings:
- print(f"⚠️ WARNINGS ({len(report.warnings)}):")
- for issue in report.warnings:
- location = f" ({issue.file}:{issue.line})" if issue.file and issue.line else f" ({issue.file})" if issue.file else ""
- print(f" [{issue.category}] {issue.message}{location}")
- print()
-
- # Info (only in verbose mode)
- if verbose and report.info:
- print(f"ℹ️ INFO ({len(report.info)}):")
- for issue in report.info:
- location = f" ({issue.file})" if issue.file else ""
- print(f" [{issue.category}] {issue.message}{location}")
- print()
-
- # Summary
- if report.is_excellent:
- print("✅ EXCELLENT! No issues found.")
- elif not report.has_errors:
- print("✓ GOOD! No errors, but some warnings to review.")
- else:
- print("❌ NEEDS IMPROVEMENT! Please fix errors before packaging.")
-
- print()
-
-
-def main():
- """Main entry point."""
- import argparse
-
- parser = argparse.ArgumentParser(
- description="Check skill quality and generate report",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Basic quality check
- python3 quality_checker.py output/react/
-
- # Verbose mode (show all info)
- python3 quality_checker.py output/godot/ --verbose
-
- # Exit with error code if issues found
- python3 quality_checker.py output/django/ --strict
-"""
- )
-
- parser.add_argument(
- 'skill_directory',
- help='Path to skill directory (e.g., output/react/)'
- )
-
- parser.add_argument(
- '--verbose', '-v',
- action='store_true',
- help='Show all info messages'
- )
-
- parser.add_argument(
- '--strict',
- action='store_true',
- help='Exit with error code if any warnings or errors found'
- )
-
- args = parser.parse_args()
-
- # Check if directory exists
- skill_dir = Path(args.skill_directory)
- if not skill_dir.exists():
- print(f"❌ Directory not found: {skill_dir}")
- sys.exit(1)
-
- # Run quality checks
- checker = SkillQualityChecker(skill_dir)
- report = checker.check_all()
-
- # Print report
- print_report(report, verbose=args.verbose)
-
- # Exit code
- if args.strict and (report.has_errors or report.has_warnings):
- sys.exit(1)
- elif report.has_errors:
- sys.exit(1)
- else:
- sys.exit(0)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/run_tests.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/run_tests.py
deleted file mode 100644
index ab38fcc..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/run_tests.py
+++ /dev/null
@@ -1,228 +0,0 @@
-#!/usr/bin/env python3
-"""
-Test Runner for Skill Seeker
-Runs all test suites and generates a comprehensive test report
-"""
-
-import sys
-import unittest
-import os
-from io import StringIO
-from pathlib import Path
-
-
-class ColoredTextTestResult(unittest.TextTestResult):
- """Custom test result class with colored output"""
-
- # ANSI color codes
- GREEN = '\033[92m'
- RED = '\033[91m'
- YELLOW = '\033[93m'
- BLUE = '\033[94m'
- RESET = '\033[0m'
- BOLD = '\033[1m'
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.test_results = []
-
- def addSuccess(self, test):
- super().addSuccess(test)
- self.test_results.append(('PASS', test))
- if self.showAll:
- self.stream.write(f"{self.GREEN}✓ PASS{self.RESET}\n")
- elif self.dots:
- self.stream.write(f"{self.GREEN}.{self.RESET}")
- self.stream.flush()
-
- def addError(self, test, err):
- super().addError(test, err)
- self.test_results.append(('ERROR', test))
- if self.showAll:
- self.stream.write(f"{self.RED}✗ ERROR{self.RESET}\n")
- elif self.dots:
- self.stream.write(f"{self.RED}E{self.RESET}")
- self.stream.flush()
-
- def addFailure(self, test, err):
- super().addFailure(test, err)
- self.test_results.append(('FAIL', test))
- if self.showAll:
- self.stream.write(f"{self.RED}✗ FAIL{self.RESET}\n")
- elif self.dots:
- self.stream.write(f"{self.RED}F{self.RESET}")
- self.stream.flush()
-
- def addSkip(self, test, reason):
- super().addSkip(test, reason)
- self.test_results.append(('SKIP', test))
- if self.showAll:
- self.stream.write(f"{self.YELLOW}⊘ SKIP{self.RESET}\n")
- elif self.dots:
- self.stream.write(f"{self.YELLOW}s{self.RESET}")
- self.stream.flush()
-
-
-class ColoredTextTestRunner(unittest.TextTestRunner):
- """Custom test runner with colored output"""
- resultclass = ColoredTextTestResult
-
-
-def discover_tests(test_dir='tests'):
- """Discover all test files in the tests directory"""
- loader = unittest.TestLoader()
- start_dir = test_dir
- pattern = 'test_*.py'
-
- suite = loader.discover(start_dir, pattern=pattern)
- return suite
-
-
-def run_specific_suite(suite_name):
- """Run a specific test suite"""
- loader = unittest.TestLoader()
-
- suite_map = {
- 'config': 'tests.test_config_validation',
- 'features': 'tests.test_scraper_features',
- 'integration': 'tests.test_integration'
- }
-
- if suite_name not in suite_map:
- print(f"Unknown test suite: {suite_name}")
- print(f"Available suites: {', '.join(suite_map.keys())}")
- return None
-
- module_name = suite_map[suite_name]
- try:
- suite = loader.loadTestsFromName(module_name)
- return suite
- except Exception as e:
- print(f"Error loading test suite '{suite_name}': {e}")
- return None
-
-
-def print_summary(result):
- """Print a detailed test summary"""
- total = result.testsRun
- passed = total - len(result.failures) - len(result.errors) - len(result.skipped)
- failed = len(result.failures)
- errors = len(result.errors)
- skipped = len(result.skipped)
-
- print("\n" + "="*70)
- print("TEST SUMMARY")
- print("="*70)
-
- # Overall stats
- print(f"\n{ColoredTextTestResult.BOLD}Total Tests:{ColoredTextTestResult.RESET} {total}")
- print(f"{ColoredTextTestResult.GREEN}✓ Passed:{ColoredTextTestResult.RESET} {passed}")
- if failed > 0:
- print(f"{ColoredTextTestResult.RED}✗ Failed:{ColoredTextTestResult.RESET} {failed}")
- if errors > 0:
- print(f"{ColoredTextTestResult.RED}✗ Errors:{ColoredTextTestResult.RESET} {errors}")
- if skipped > 0:
- print(f"{ColoredTextTestResult.YELLOW}⊘ Skipped:{ColoredTextTestResult.RESET} {skipped}")
-
- # Success rate
- if total > 0:
- success_rate = (passed / total) * 100
- color = ColoredTextTestResult.GREEN if success_rate == 100 else \
- ColoredTextTestResult.YELLOW if success_rate >= 80 else \
- ColoredTextTestResult.RED
- print(f"\n{color}Success Rate: {success_rate:.1f}%{ColoredTextTestResult.RESET}")
-
- # Category breakdown
- if hasattr(result, 'test_results'):
- print(f"\n{ColoredTextTestResult.BOLD}Test Breakdown by Category:{ColoredTextTestResult.RESET}")
-
- categories = {}
- for status, test in result.test_results:
- test_name = str(test)
- # Extract test class name
- if '.' in test_name:
- class_name = test_name.split('.')[0].split()[-1]
- if class_name not in categories:
- categories[class_name] = {'PASS': 0, 'FAIL': 0, 'ERROR': 0, 'SKIP': 0}
- categories[class_name][status] += 1
-
- for category, stats in sorted(categories.items()):
- total_cat = sum(stats.values())
- passed_cat = stats['PASS']
- print(f" {category}: {passed_cat}/{total_cat} passed")
-
- print("\n" + "="*70)
-
- # Return status
- return failed == 0 and errors == 0
-
-
-def main():
- """Main test runner"""
- import argparse
-
- parser = argparse.ArgumentParser(
- description='Run tests for Skill Seeker',
- formatter_class=argparse.RawDescriptionHelpFormatter
- )
-
- parser.add_argument('--suite', '-s', type=str,
- help='Run specific test suite (config, features, integration)')
- parser.add_argument('--verbose', '-v', action='store_true',
- help='Verbose output (show each test)')
- parser.add_argument('--quiet', '-q', action='store_true',
- help='Quiet output (minimal output)')
- parser.add_argument('--failfast', '-f', action='store_true',
- help='Stop on first failure')
- parser.add_argument('--list', '-l', action='store_true',
- help='List all available tests')
-
- args = parser.parse_args()
-
- # Set verbosity
- verbosity = 1
- if args.verbose:
- verbosity = 2
- elif args.quiet:
- verbosity = 0
-
- print(f"\n{ColoredTextTestResult.BOLD}{'='*70}{ColoredTextTestResult.RESET}")
- print(f"{ColoredTextTestResult.BOLD}SKILL SEEKER TEST SUITE{ColoredTextTestResult.RESET}")
- print(f"{ColoredTextTestResult.BOLD}{'='*70}{ColoredTextTestResult.RESET}\n")
-
- # Discover or load specific suite
- if args.suite:
- print(f"Running test suite: {ColoredTextTestResult.BLUE}{args.suite}{ColoredTextTestResult.RESET}\n")
- suite = run_specific_suite(args.suite)
- if suite is None:
- return 1
- else:
- print(f"Running {ColoredTextTestResult.BLUE}all tests{ColoredTextTestResult.RESET}\n")
- suite = discover_tests()
-
- # List tests
- if args.list:
- print("\nAvailable tests:\n")
- for test_group in suite:
- for test in test_group:
- print(f" - {test}")
- print()
- return 0
-
- # Run tests
- runner = ColoredTextTestRunner(
- verbosity=verbosity,
- failfast=args.failfast
- )
-
- result = runner.run(suite)
-
- # Print summary
- success = print_summary(result)
-
- # Return appropriate exit code
- return 0 if success else 1
-
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/split_config.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/split_config.py
deleted file mode 100644
index 40551ad..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/split_config.py
+++ /dev/null
@@ -1,320 +0,0 @@
-#!/usr/bin/env python3
-"""
-Config Splitter for Large Documentation Sites
-
-Splits large documentation configs into multiple smaller, focused skill configs.
-Supports multiple splitting strategies: category-based, size-based, and automatic.
-"""
-
-import json
-import sys
-import argparse
-from pathlib import Path
-from typing import Dict, List, Any, Tuple
-from collections import defaultdict
-
-
-class ConfigSplitter:
- """Splits large documentation configs into multiple focused configs"""
-
- def __init__(self, config_path: str, strategy: str = "auto", target_pages: int = 5000):
- self.config_path = Path(config_path)
- self.strategy = strategy
- self.target_pages = target_pages
- self.config = self.load_config()
- self.base_name = self.config['name']
-
- def load_config(self) -> Dict[str, Any]:
- """Load configuration from file"""
- try:
- with open(self.config_path, 'r') as f:
- return json.load(f)
- except FileNotFoundError:
- print(f"❌ Error: Config file not found: {self.config_path}")
- sys.exit(1)
- except json.JSONDecodeError as e:
- print(f"❌ Error: Invalid JSON in config file: {e}")
- sys.exit(1)
-
- def get_split_strategy(self) -> str:
- """Determine split strategy"""
- # Check if strategy is defined in config
- if 'split_strategy' in self.config:
- config_strategy = self.config['split_strategy']
- if config_strategy != "none":
- return config_strategy
-
- # Use provided strategy or auto-detect
- if self.strategy == "auto":
- max_pages = self.config.get('max_pages', 500)
-
- if max_pages < 5000:
- print(f"ℹ️ Small documentation ({max_pages} pages) - no splitting needed")
- return "none"
- elif max_pages < 10000 and 'categories' in self.config:
- print(f"ℹ️ Medium documentation ({max_pages} pages) - category split recommended")
- return "category"
- elif 'categories' in self.config and len(self.config['categories']) >= 3:
- print(f"ℹ️ Large documentation ({max_pages} pages) - router + categories recommended")
- return "router"
- else:
- print(f"ℹ️ Large documentation ({max_pages} pages) - size-based split")
- return "size"
-
- return self.strategy
-
- def split_by_category(self, create_router: bool = False) -> List[Dict[str, Any]]:
- """Split config by categories"""
- if 'categories' not in self.config:
- print("❌ Error: No categories defined in config")
- sys.exit(1)
-
- categories = self.config['categories']
- split_categories = self.config.get('split_config', {}).get('split_by_categories')
-
- # If specific categories specified, use only those
- if split_categories:
- categories = {k: v for k, v in categories.items() if k in split_categories}
-
- configs = []
-
- for category_name, keywords in categories.items():
- # Create new config for this category
- new_config = self.config.copy()
- new_config['name'] = f"{self.base_name}-{category_name}"
- new_config['description'] = f"{self.base_name.capitalize()} - {category_name.replace('_', ' ').title()}. {self.config.get('description', '')}"
-
- # Update URL patterns to focus on this category
- url_patterns = new_config.get('url_patterns', {})
-
- # Add category keywords to includes
- includes = url_patterns.get('include', [])
- for keyword in keywords:
- if keyword.startswith('/'):
- includes.append(keyword)
-
- if includes:
- url_patterns['include'] = list(set(includes))
- new_config['url_patterns'] = url_patterns
-
- # Keep only this category
- new_config['categories'] = {category_name: keywords}
-
- # Remove split config from child
- if 'split_strategy' in new_config:
- del new_config['split_strategy']
- if 'split_config' in new_config:
- del new_config['split_config']
-
- # Adjust max_pages estimate
- if 'max_pages' in new_config:
- new_config['max_pages'] = self.target_pages
-
- configs.append(new_config)
-
- print(f"✅ Created {len(configs)} category-based configs")
-
- # Optionally create router config
- if create_router:
- router_config = self.create_router_config(configs)
- configs.insert(0, router_config)
- print(f"✅ Created router config: {router_config['name']}")
-
- return configs
-
- def split_by_size(self) -> List[Dict[str, Any]]:
- """Split config by size (page count)"""
- max_pages = self.config.get('max_pages', 500)
- num_splits = (max_pages + self.target_pages - 1) // self.target_pages
-
- configs = []
-
- for i in range(num_splits):
- new_config = self.config.copy()
- part_num = i + 1
- new_config['name'] = f"{self.base_name}-part{part_num}"
- new_config['description'] = f"{self.base_name.capitalize()} - Part {part_num}. {self.config.get('description', '')}"
- new_config['max_pages'] = self.target_pages
-
- # Remove split config from child
- if 'split_strategy' in new_config:
- del new_config['split_strategy']
- if 'split_config' in new_config:
- del new_config['split_config']
-
- configs.append(new_config)
-
- print(f"✅ Created {len(configs)} size-based configs ({self.target_pages} pages each)")
- return configs
-
- def create_router_config(self, sub_configs: List[Dict[str, Any]]) -> Dict[str, Any]:
- """Create a router config that references sub-skills"""
- router_name = self.config.get('split_config', {}).get('router_name', self.base_name)
-
- router_config = {
- "name": router_name,
- "description": self.config.get('description', ''),
- "base_url": self.config['base_url'],
- "selectors": self.config['selectors'],
- "url_patterns": self.config.get('url_patterns', {}),
- "rate_limit": self.config.get('rate_limit', 0.5),
- "max_pages": 500, # Router only needs overview pages
- "_router": True,
- "_sub_skills": [cfg['name'] for cfg in sub_configs],
- "_routing_keywords": {
- cfg['name']: list(cfg.get('categories', {}).keys())
- for cfg in sub_configs
- }
- }
-
- return router_config
-
- def split(self) -> List[Dict[str, Any]]:
- """Execute split based on strategy"""
- strategy = self.get_split_strategy()
-
- print(f"\n{'='*60}")
- print(f"CONFIG SPLITTER: {self.base_name}")
- print(f"{'='*60}")
- print(f"Strategy: {strategy}")
- print(f"Target pages per skill: {self.target_pages}")
- print("")
-
- if strategy == "none":
- print("ℹ️ No splitting required")
- return [self.config]
-
- elif strategy == "category":
- return self.split_by_category(create_router=False)
-
- elif strategy == "router":
- create_router = self.config.get('split_config', {}).get('create_router', True)
- return self.split_by_category(create_router=create_router)
-
- elif strategy == "size":
- return self.split_by_size()
-
- else:
- print(f"❌ Error: Unknown strategy: {strategy}")
- sys.exit(1)
-
- def save_configs(self, configs: List[Dict[str, Any]], output_dir: Path = None) -> List[Path]:
- """Save configs to files"""
- if output_dir is None:
- output_dir = self.config_path.parent
-
- output_dir = Path(output_dir)
- output_dir.mkdir(parents=True, exist_ok=True)
-
- saved_files = []
-
- for config in configs:
- filename = f"{config['name']}.json"
- filepath = output_dir / filename
-
- with open(filepath, 'w') as f:
- json.dump(config, f, indent=2)
-
- saved_files.append(filepath)
- print(f" 💾 Saved: {filepath}")
-
- return saved_files
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Split large documentation configs into multiple focused skills",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Auto-detect strategy
- python3 split_config.py configs/godot.json
-
- # Use category-based split
- python3 split_config.py configs/godot.json --strategy category
-
- # Use router + categories
- python3 split_config.py configs/godot.json --strategy router
-
- # Custom target size
- python3 split_config.py configs/godot.json --target-pages 3000
-
- # Dry run (don't save files)
- python3 split_config.py configs/godot.json --dry-run
-
-Split Strategies:
- none - No splitting (single skill)
- auto - Automatically choose best strategy
- category - Split by categories defined in config
- router - Create router + category-based sub-skills
- size - Split by page count
- """
- )
-
- parser.add_argument(
- 'config',
- help='Path to config file (e.g., configs/godot.json)'
- )
-
- parser.add_argument(
- '--strategy',
- choices=['auto', 'none', 'category', 'router', 'size'],
- default='auto',
- help='Splitting strategy (default: auto)'
- )
-
- parser.add_argument(
- '--target-pages',
- type=int,
- default=5000,
- help='Target pages per skill (default: 5000)'
- )
-
- parser.add_argument(
- '--output-dir',
- help='Output directory for configs (default: same as input)'
- )
-
- parser.add_argument(
- '--dry-run',
- action='store_true',
- help='Show what would be created without saving files'
- )
-
- args = parser.parse_args()
-
- # Create splitter
- splitter = ConfigSplitter(args.config, args.strategy, args.target_pages)
-
- # Split config
- configs = splitter.split()
-
- if args.dry_run:
- print(f"\n{'='*60}")
- print("DRY RUN - No files saved")
- print(f"{'='*60}")
- print(f"Would create {len(configs)} config files:")
- for cfg in configs:
- is_router = cfg.get('_router', False)
- router_marker = " (ROUTER)" if is_router else ""
- print(f" 📄 {cfg['name']}.json{router_marker}")
- else:
- print(f"\n{'='*60}")
- print("SAVING CONFIGS")
- print(f"{'='*60}")
- saved_files = splitter.save_configs(configs, args.output_dir)
-
- print(f"\n{'='*60}")
- print("NEXT STEPS")
- print(f"{'='*60}")
- print("1. Review generated configs")
- print("2. Scrape each config:")
- for filepath in saved_files:
- print(f" skill-seekers scrape --config {filepath}")
- print("3. Package skills:")
- print(" skill-seekers-package-multi configs/-*.json")
- print("")
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/test_unified_simple.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/test_unified_simple.py
deleted file mode 100644
index f759fd1..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/test_unified_simple.py
+++ /dev/null
@@ -1,192 +0,0 @@
-#!/usr/bin/env python3
-"""
-Simple Integration Tests for Unified Multi-Source Scraper
-
-Focuses on real-world usage patterns rather than unit tests.
-"""
-
-import os
-import sys
-import json
-import tempfile
-from pathlib import Path
-
-# Add CLI to path
-sys.path.insert(0, str(Path(__file__).parent))
-
-from .config_validator import validate_config
-
-def test_validate_existing_unified_configs():
- """Test that all existing unified configs are valid"""
- configs_dir = Path(__file__).parent.parent / 'configs'
-
- unified_configs = [
- 'godot_unified.json',
- 'react_unified.json',
- 'django_unified.json',
- 'fastapi_unified.json'
- ]
-
- for config_name in unified_configs:
- config_path = configs_dir / config_name
- if config_path.exists():
- print(f"\n✓ Validating {config_name}...")
- validator = validate_config(str(config_path))
- assert validator.is_unified, f"{config_name} should be unified format"
- assert validator.needs_api_merge(), f"{config_name} should need API merging"
- print(f" Sources: {len(validator.config['sources'])}")
- print(f" Merge mode: {validator.config.get('merge_mode')}")
-
-
-def test_backward_compatibility():
- """Test that legacy configs still work"""
- configs_dir = Path(__file__).parent.parent / 'configs'
-
- legacy_configs = [
- 'react.json',
- 'godot.json',
- 'django.json'
- ]
-
- for config_name in legacy_configs:
- config_path = configs_dir / config_name
- if config_path.exists():
- print(f"\n✓ Validating legacy {config_name}...")
- validator = validate_config(str(config_path))
- assert not validator.is_unified, f"{config_name} should be legacy format"
- print(f" Format: Legacy")
-
-
-def test_create_temp_unified_config():
- """Test creating a unified config from scratch"""
- config = {
- "name": "test_unified",
- "description": "Test unified config",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://example.com/docs",
- "extract_api": True,
- "max_pages": 50
- },
- {
- "type": "github",
- "repo": "test/repo",
- "include_code": True,
- "code_analysis_depth": "surface"
- }
- ]
- }
-
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(config, f)
- config_path = f.name
-
- try:
- print("\n✓ Validating temp unified config...")
- validator = validate_config(config_path)
- assert validator.is_unified
- assert validator.needs_api_merge()
- assert len(validator.config['sources']) == 2
- print(" ✓ Config is valid unified format")
- print(f" Sources: {len(validator.config['sources'])}")
- finally:
- os.unlink(config_path)
-
-
-def test_mixed_source_types():
- """Test config with documentation, GitHub, and PDF sources"""
- config = {
- "name": "test_mixed",
- "description": "Test mixed sources",
- "merge_mode": "rule-based",
- "sources": [
- {
- "type": "documentation",
- "base_url": "https://example.com"
- },
- {
- "type": "github",
- "repo": "test/repo"
- },
- {
- "type": "pdf",
- "path": "/path/to/manual.pdf"
- }
- ]
- }
-
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(config, f)
- config_path = f.name
-
- try:
- print("\n✓ Validating mixed source types...")
- validator = validate_config(config_path)
- assert validator.is_unified
- assert len(validator.config['sources']) == 3
-
- # Check each source type
- source_types = [s['type'] for s in validator.config['sources']]
- assert 'documentation' in source_types
- assert 'github' in source_types
- assert 'pdf' in source_types
- print(" ✓ All 3 source types validated")
- finally:
- os.unlink(config_path)
-
-
-def test_config_validation_errors():
- """Test that invalid configs are rejected"""
- # Invalid source type
- config = {
- "name": "test",
- "description": "Test",
- "sources": [
- {"type": "invalid_type", "url": "https://example.com"}
- ]
- }
-
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
- json.dump(config, f)
- config_path = f.name
-
- try:
- print("\n✓ Testing invalid source type...")
- try:
- # validate_config() calls .validate() automatically
- validator = validate_config(config_path)
- assert False, "Should have raised error for invalid source type"
- except ValueError as e:
- assert "Invalid" in str(e) or "invalid" in str(e)
- print(" ✓ Invalid source type correctly rejected")
- finally:
- os.unlink(config_path)
-
-
-# Run tests
-if __name__ == '__main__':
- print("=" * 60)
- print("Running Unified Scraper Integration Tests")
- print("=" * 60)
-
- try:
- test_validate_existing_unified_configs()
- test_backward_compatibility()
- test_create_temp_unified_config()
- test_mixed_source_types()
- test_config_validation_errors()
-
- print("\n" + "=" * 60)
- print("✅ All integration tests passed!")
- print("=" * 60)
-
- except AssertionError as e:
- print(f"\n❌ Test failed: {e}")
- sys.exit(1)
- except Exception as e:
- print(f"\n❌ Unexpected error: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/unified_scraper.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/unified_scraper.py
deleted file mode 100644
index 81d2bc1..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/unified_scraper.py
+++ /dev/null
@@ -1,450 +0,0 @@
-#!/usr/bin/env python3
-"""
-Unified Multi-Source Scraper
-
-Orchestrates scraping from multiple sources (documentation, GitHub, PDF),
-detects conflicts, merges intelligently, and builds unified skills.
-
-This is the main entry point for unified config workflow.
-
-Usage:
- skill-seekers unified --config configs/godot_unified.json
- skill-seekers unified --config configs/react_unified.json --merge-mode claude-enhanced
-"""
-
-import os
-import sys
-import json
-import logging
-import argparse
-import subprocess
-from pathlib import Path
-from typing import Dict, List, Any, Optional
-
-# Import validators and scrapers
-try:
- from config_validator import ConfigValidator, validate_config
- from conflict_detector import ConflictDetector
- from merge_sources import RuleBasedMerger, ClaudeEnhancedMerger
- from unified_skill_builder import UnifiedSkillBuilder
-except ImportError as e:
- print(f"Error importing modules: {e}")
- print("Make sure you're running from the project root directory")
- sys.exit(1)
-
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-
-class UnifiedScraper:
- """
- Orchestrates multi-source scraping and merging.
-
- Main workflow:
- 1. Load and validate unified config
- 2. Scrape all sources (docs, GitHub, PDF)
- 3. Detect conflicts between sources
- 4. Merge intelligently (rule-based or Claude-enhanced)
- 5. Build unified skill
- """
-
- def __init__(self, config_path: str, merge_mode: Optional[str] = None):
- """
- Initialize unified scraper.
-
- Args:
- config_path: Path to unified config JSON
- merge_mode: Override config merge_mode ('rule-based' or 'claude-enhanced')
- """
- self.config_path = config_path
-
- # Validate and load config
- logger.info(f"Loading config: {config_path}")
- self.validator = validate_config(config_path)
- self.config = self.validator.config
-
- # Determine merge mode
- self.merge_mode = merge_mode or self.config.get('merge_mode', 'rule-based')
- logger.info(f"Merge mode: {self.merge_mode}")
-
- # Storage for scraped data
- self.scraped_data = {}
-
- # Output paths
- self.name = self.config['name']
- self.output_dir = f"output/{self.name}"
- self.data_dir = f"output/{self.name}_unified_data"
-
- os.makedirs(self.output_dir, exist_ok=True)
- os.makedirs(self.data_dir, exist_ok=True)
-
- def scrape_all_sources(self):
- """
- Scrape all configured sources.
-
- Routes to appropriate scraper based on source type.
- """
- logger.info("=" * 60)
- logger.info("PHASE 1: Scraping all sources")
- logger.info("=" * 60)
-
- if not self.validator.is_unified:
- logger.warning("Config is not unified format, converting...")
- self.config = self.validator.convert_legacy_to_unified()
-
- sources = self.config.get('sources', [])
-
- for i, source in enumerate(sources):
- source_type = source['type']
- logger.info(f"\n[{i+1}/{len(sources)}] Scraping {source_type} source...")
-
- try:
- if source_type == 'documentation':
- self._scrape_documentation(source)
- elif source_type == 'github':
- self._scrape_github(source)
- elif source_type == 'pdf':
- self._scrape_pdf(source)
- else:
- logger.warning(f"Unknown source type: {source_type}")
- except Exception as e:
- logger.error(f"Error scraping {source_type}: {e}")
- logger.info("Continuing with other sources...")
-
- logger.info(f"\n✅ Scraped {len(self.scraped_data)} sources successfully")
-
- def _scrape_documentation(self, source: Dict[str, Any]):
- """Scrape documentation website."""
- # Create temporary config for doc scraper
- doc_config = {
- 'name': f"{self.name}_docs",
- 'base_url': source['base_url'],
- 'selectors': source.get('selectors', {}),
- 'url_patterns': source.get('url_patterns', {}),
- 'categories': source.get('categories', {}),
- 'rate_limit': source.get('rate_limit', 0.5),
- 'max_pages': source.get('max_pages', 100)
- }
-
- # Write temporary config
- temp_config_path = os.path.join(self.data_dir, 'temp_docs_config.json')
- with open(temp_config_path, 'w') as f:
- json.dump(doc_config, f, indent=2)
-
- # Run doc_scraper as subprocess
- logger.info(f"Scraping documentation from {source['base_url']}")
-
- doc_scraper_path = Path(__file__).parent / "doc_scraper.py"
- cmd = [sys.executable, str(doc_scraper_path), '--config', temp_config_path]
-
- result = subprocess.run(cmd, capture_output=True, text=True)
-
- if result.returncode != 0:
- logger.error(f"Documentation scraping failed: {result.stderr}")
- return
-
- # Load scraped data
- docs_data_file = f"output/{doc_config['name']}_data/summary.json"
-
- if os.path.exists(docs_data_file):
- with open(docs_data_file, 'r') as f:
- summary = json.load(f)
-
- self.scraped_data['documentation'] = {
- 'pages': summary.get('pages', []),
- 'data_file': docs_data_file
- }
-
- logger.info(f"✅ Documentation: {summary.get('total_pages', 0)} pages scraped")
- else:
- logger.warning("Documentation data file not found")
-
- # Clean up temp config
- if os.path.exists(temp_config_path):
- os.remove(temp_config_path)
-
- def _scrape_github(self, source: Dict[str, Any]):
- """Scrape GitHub repository."""
- sys.path.insert(0, str(Path(__file__).parent))
-
- try:
- from github_scraper import GitHubScraper
- except ImportError:
- logger.error("github_scraper.py not found")
- return
-
- # Create config for GitHub scraper
- github_config = {
- 'repo': source['repo'],
- 'name': f"{self.name}_github",
- 'github_token': source.get('github_token'),
- 'include_issues': source.get('include_issues', True),
- 'max_issues': source.get('max_issues', 100),
- 'include_changelog': source.get('include_changelog', True),
- 'include_releases': source.get('include_releases', True),
- 'include_code': source.get('include_code', True),
- 'code_analysis_depth': source.get('code_analysis_depth', 'surface'),
- 'file_patterns': source.get('file_patterns', []),
- 'local_repo_path': source.get('local_repo_path') # Pass local_repo_path from config
- }
-
- # Scrape
- logger.info(f"Scraping GitHub repository: {source['repo']}")
- scraper = GitHubScraper(github_config)
- github_data = scraper.scrape()
-
- # Save data
- github_data_file = os.path.join(self.data_dir, 'github_data.json')
- with open(github_data_file, 'w') as f:
- json.dump(github_data, f, indent=2, ensure_ascii=False)
-
- self.scraped_data['github'] = {
- 'data': github_data,
- 'data_file': github_data_file
- }
-
- logger.info(f"✅ GitHub: Repository scraped successfully")
-
- def _scrape_pdf(self, source: Dict[str, Any]):
- """Scrape PDF document."""
- sys.path.insert(0, str(Path(__file__).parent))
-
- try:
- from pdf_scraper import PDFToSkillConverter
- except ImportError:
- logger.error("pdf_scraper.py not found")
- return
-
- # Create config for PDF scraper
- pdf_config = {
- 'name': f"{self.name}_pdf",
- 'pdf': source['path'],
- 'extract_tables': source.get('extract_tables', False),
- 'ocr': source.get('ocr', False),
- 'password': source.get('password')
- }
-
- # Scrape
- logger.info(f"Scraping PDF: {source['path']}")
- converter = PDFToSkillConverter(pdf_config)
- pdf_data = converter.extract_all()
-
- # Save data
- pdf_data_file = os.path.join(self.data_dir, 'pdf_data.json')
- with open(pdf_data_file, 'w') as f:
- json.dump(pdf_data, f, indent=2, ensure_ascii=False)
-
- self.scraped_data['pdf'] = {
- 'data': pdf_data,
- 'data_file': pdf_data_file
- }
-
- logger.info(f"✅ PDF: {len(pdf_data.get('pages', []))} pages extracted")
-
- def detect_conflicts(self) -> List:
- """
- Detect conflicts between documentation and code.
-
- Only applicable if both documentation and GitHub sources exist.
-
- Returns:
- List of conflicts
- """
- logger.info("\n" + "=" * 60)
- logger.info("PHASE 2: Detecting conflicts")
- logger.info("=" * 60)
-
- if not self.validator.needs_api_merge():
- logger.info("No API merge needed (only one API source)")
- return []
-
- # Get documentation and GitHub data
- docs_data = self.scraped_data.get('documentation', {})
- github_data = self.scraped_data.get('github', {})
-
- if not docs_data or not github_data:
- logger.warning("Missing documentation or GitHub data for conflict detection")
- return []
-
- # Load data files
- with open(docs_data['data_file'], 'r') as f:
- docs_json = json.load(f)
-
- with open(github_data['data_file'], 'r') as f:
- github_json = json.load(f)
-
- # Detect conflicts
- detector = ConflictDetector(docs_json, github_json)
- conflicts = detector.detect_all_conflicts()
-
- # Save conflicts
- conflicts_file = os.path.join(self.data_dir, 'conflicts.json')
- detector.save_conflicts(conflicts, conflicts_file)
-
- # Print summary
- summary = detector.generate_summary(conflicts)
- logger.info(f"\n📊 Conflict Summary:")
- logger.info(f" Total: {summary['total']}")
- logger.info(f" By Type:")
- for ctype, count in summary['by_type'].items():
- if count > 0:
- logger.info(f" - {ctype}: {count}")
- logger.info(f" By Severity:")
- for severity, count in summary['by_severity'].items():
- if count > 0:
- emoji = '🔴' if severity == 'high' else '🟡' if severity == 'medium' else '🟢'
- logger.info(f" {emoji} {severity}: {count}")
-
- return conflicts
-
- def merge_sources(self, conflicts: List):
- """
- Merge data from multiple sources.
-
- Args:
- conflicts: List of detected conflicts
- """
- logger.info("\n" + "=" * 60)
- logger.info(f"PHASE 3: Merging sources ({self.merge_mode})")
- logger.info("=" * 60)
-
- if not conflicts:
- logger.info("No conflicts to merge")
- return None
-
- # Get data files
- docs_data = self.scraped_data.get('documentation', {})
- github_data = self.scraped_data.get('github', {})
-
- # Load data
- with open(docs_data['data_file'], 'r') as f:
- docs_json = json.load(f)
-
- with open(github_data['data_file'], 'r') as f:
- github_json = json.load(f)
-
- # Choose merger
- if self.merge_mode == 'claude-enhanced':
- merger = ClaudeEnhancedMerger(docs_json, github_json, conflicts)
- else:
- merger = RuleBasedMerger(docs_json, github_json, conflicts)
-
- # Merge
- merged_data = merger.merge_all()
-
- # Save merged data
- merged_file = os.path.join(self.data_dir, 'merged_data.json')
- with open(merged_file, 'w') as f:
- json.dump(merged_data, f, indent=2, ensure_ascii=False)
-
- logger.info(f"✅ Merged data saved: {merged_file}")
-
- return merged_data
-
- def build_skill(self, merged_data: Optional[Dict] = None):
- """
- Build final unified skill.
-
- Args:
- merged_data: Merged API data (if conflicts were resolved)
- """
- logger.info("\n" + "=" * 60)
- logger.info("PHASE 4: Building unified skill")
- logger.info("=" * 60)
-
- # Load conflicts if they exist
- conflicts = []
- conflicts_file = os.path.join(self.data_dir, 'conflicts.json')
- if os.path.exists(conflicts_file):
- with open(conflicts_file, 'r') as f:
- conflicts_data = json.load(f)
- conflicts = conflicts_data.get('conflicts', [])
-
- # Build skill
- builder = UnifiedSkillBuilder(
- self.config,
- self.scraped_data,
- merged_data,
- conflicts
- )
-
- builder.build()
-
- logger.info(f"✅ Unified skill built: {self.output_dir}/")
-
- def run(self):
- """
- Execute complete unified scraping workflow.
- """
- logger.info("\n" + "🚀 " * 20)
- logger.info(f"Unified Scraper: {self.config['name']}")
- logger.info("🚀 " * 20 + "\n")
-
- try:
- # Phase 1: Scrape all sources
- self.scrape_all_sources()
-
- # Phase 2: Detect conflicts (if applicable)
- conflicts = self.detect_conflicts()
-
- # Phase 3: Merge sources (if conflicts exist)
- merged_data = None
- if conflicts:
- merged_data = self.merge_sources(conflicts)
-
- # Phase 4: Build skill
- self.build_skill(merged_data)
-
- logger.info("\n" + "✅ " * 20)
- logger.info("Unified scraping complete!")
- logger.info("✅ " * 20 + "\n")
-
- logger.info(f"📁 Output: {self.output_dir}/")
- logger.info(f"📁 Data: {self.data_dir}/")
-
- except KeyboardInterrupt:
- logger.info("\n\n⚠️ Scraping interrupted by user")
- sys.exit(1)
- except Exception as e:
- logger.error(f"\n\n❌ Error during scraping: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
-
-
-def main():
- """Main entry point."""
- parser = argparse.ArgumentParser(
- description='Unified multi-source scraper',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- # Basic usage with unified config
- skill-seekers unified --config configs/godot_unified.json
-
- # Override merge mode
- skill-seekers unified --config configs/react_unified.json --merge-mode claude-enhanced
-
- # Backward compatible with legacy configs
- skill-seekers unified --config configs/react.json
- """
- )
-
- parser.add_argument('--config', '-c', required=True,
- help='Path to unified config JSON file')
- parser.add_argument('--merge-mode', '-m',
- choices=['rule-based', 'claude-enhanced'],
- help='Override config merge mode')
-
- args = parser.parse_args()
-
- # Create and run scraper
- scraper = UnifiedScraper(args.config, args.merge_mode)
- scraper.run()
-
-
-if __name__ == '__main__':
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/unified_skill_builder.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/unified_skill_builder.py
deleted file mode 100644
index dd3051d..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/unified_skill_builder.py
+++ /dev/null
@@ -1,444 +0,0 @@
-#!/usr/bin/env python3
-"""
-Unified Skill Builder
-
-Generates final skill structure from merged multi-source data:
-- SKILL.md with merged APIs and conflict warnings
-- references/ with organized content by source
-- Inline conflict markers (⚠️)
-- Separate conflicts summary section
-
-Supports mixed sources (documentation, GitHub, PDF) and highlights
-discrepancies transparently.
-"""
-
-import os
-import json
-import logging
-from pathlib import Path
-from typing import Dict, List, Any, Optional
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-class UnifiedSkillBuilder:
- """
- Builds unified skill from multi-source data.
- """
-
- def __init__(self, config: Dict, scraped_data: Dict,
- merged_data: Optional[Dict] = None, conflicts: Optional[List] = None):
- """
- Initialize skill builder.
-
- Args:
- config: Unified config dict
- scraped_data: Dict of scraped data by source type
- merged_data: Merged API data (if conflicts were resolved)
- conflicts: List of detected conflicts
- """
- self.config = config
- self.scraped_data = scraped_data
- self.merged_data = merged_data
- self.conflicts = conflicts or []
-
- self.name = config['name']
- self.description = config['description']
- self.skill_dir = f"output/{self.name}"
-
- # Create directories
- os.makedirs(self.skill_dir, exist_ok=True)
- os.makedirs(f"{self.skill_dir}/references", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/scripts", exist_ok=True)
- os.makedirs(f"{self.skill_dir}/assets", exist_ok=True)
-
- def build(self):
- """Build complete skill structure."""
- logger.info(f"Building unified skill: {self.name}")
-
- # Generate main SKILL.md
- self._generate_skill_md()
-
- # Generate reference files by source
- self._generate_references()
-
- # Generate conflicts report (if any)
- if self.conflicts:
- self._generate_conflicts_report()
-
- logger.info(f"✅ Unified skill built: {self.skill_dir}/")
-
- def _generate_skill_md(self):
- """Generate main SKILL.md file."""
- skill_path = os.path.join(self.skill_dir, 'SKILL.md')
-
- # Generate skill name (lowercase, hyphens only, max 64 chars)
- skill_name = self.name.lower().replace('_', '-').replace(' ', '-')[:64]
-
- # Truncate description to 1024 chars if needed
- desc = self.description[:1024] if len(self.description) > 1024 else self.description
-
- content = f"""---
-name: {skill_name}
-description: {desc}
----
-
-# {self.name.title()}
-
-{self.description}
-
-## 📚 Sources
-
-This skill combines knowledge from multiple sources:
-
-"""
-
- # List sources
- for source in self.config.get('sources', []):
- source_type = source['type']
- if source_type == 'documentation':
- content += f"- ✅ **Documentation**: {source.get('base_url', 'N/A')}\n"
- content += f" - Pages: {source.get('max_pages', 'unlimited')}\n"
- elif source_type == 'github':
- content += f"- ✅ **GitHub Repository**: {source.get('repo', 'N/A')}\n"
- content += f" - Code Analysis: {source.get('code_analysis_depth', 'surface')}\n"
- content += f" - Issues: {source.get('max_issues', 0)}\n"
- elif source_type == 'pdf':
- content += f"- ✅ **PDF Document**: {source.get('path', 'N/A')}\n"
-
- # Data quality section
- if self.conflicts:
- content += f"\n## ⚠️ Data Quality\n\n"
- content += f"**{len(self.conflicts)} conflicts detected** between sources.\n\n"
-
- # Count by type
- by_type = {}
- for conflict in self.conflicts:
- ctype = conflict.type if hasattr(conflict, 'type') else conflict.get('type', 'unknown')
- by_type[ctype] = by_type.get(ctype, 0) + 1
-
- content += "**Conflict Breakdown:**\n"
- for ctype, count in by_type.items():
- content += f"- {ctype}: {count}\n"
-
- content += f"\nSee `references/conflicts.md` for detailed conflict information.\n"
-
- # Merged API section (if available)
- if self.merged_data:
- content += self._format_merged_apis()
-
- # Quick reference from each source
- content += "\n## 📖 Reference Documentation\n\n"
- content += "Organized by source:\n\n"
-
- for source in self.config.get('sources', []):
- source_type = source['type']
- content += f"- [{source_type.title()}](references/{source_type}/)\n"
-
- # When to use this skill
- content += f"\n## 💡 When to Use This Skill\n\n"
- content += f"Use this skill when you need to:\n"
- content += f"- Understand how to use {self.name}\n"
- content += f"- Look up API documentation\n"
- content += f"- Find usage examples\n"
-
- if 'github' in self.scraped_data:
- content += f"- Check for known issues or recent changes\n"
- content += f"- Review release history\n"
-
- content += "\n---\n\n"
- content += "*Generated by Skill Seeker's unified multi-source scraper*\n"
-
- with open(skill_path, 'w', encoding='utf-8') as f:
- f.write(content)
-
- logger.info(f"Created SKILL.md")
-
- def _format_merged_apis(self) -> str:
- """Format merged APIs section with inline conflict warnings."""
- if not self.merged_data:
- return ""
-
- content = "\n## 🔧 API Reference\n\n"
- content += "*Merged from documentation and code analysis*\n\n"
-
- apis = self.merged_data.get('apis', {})
-
- if not apis:
- return content + "*No APIs to display*\n"
-
- # Group APIs by status
- matched = {k: v for k, v in apis.items() if v.get('status') == 'matched'}
- conflicts = {k: v for k, v in apis.items() if v.get('status') == 'conflict'}
- docs_only = {k: v for k, v in apis.items() if v.get('status') == 'docs_only'}
- code_only = {k: v for k, v in apis.items() if v.get('status') == 'code_only'}
-
- # Show matched APIs first
- if matched:
- content += "### ✅ Verified APIs\n\n"
- content += "*Documentation and code agree*\n\n"
- for api_name, api_data in list(matched.items())[:10]: # Limit to first 10
- content += self._format_api_entry(api_data, inline_conflict=False)
-
- # Show conflicting APIs with warnings
- if conflicts:
- content += "\n### ⚠️ APIs with Conflicts\n\n"
- content += "*Documentation and code differ*\n\n"
- for api_name, api_data in list(conflicts.items())[:10]:
- content += self._format_api_entry(api_data, inline_conflict=True)
-
- # Show undocumented APIs
- if code_only:
- content += f"\n### 💻 Undocumented APIs\n\n"
- content += f"*Found in code but not in documentation ({len(code_only)} total)*\n\n"
- for api_name, api_data in list(code_only.items())[:5]:
- content += self._format_api_entry(api_data, inline_conflict=False)
-
- # Show removed/missing APIs
- if docs_only:
- content += f"\n### 📖 Documentation-Only APIs\n\n"
- content += f"*Documented but not found in code ({len(docs_only)} total)*\n\n"
- for api_name, api_data in list(docs_only.items())[:5]:
- content += self._format_api_entry(api_data, inline_conflict=False)
-
- content += f"\n*See references/api/ for complete API documentation*\n"
-
- return content
-
- def _format_api_entry(self, api_data: Dict, inline_conflict: bool = False) -> str:
- """Format a single API entry."""
- name = api_data.get('name', 'Unknown')
- signature = api_data.get('merged_signature', name)
- description = api_data.get('merged_description', '')
- warning = api_data.get('warning', '')
-
- entry = f"#### `{signature}`\n\n"
-
- if description:
- entry += f"{description}\n\n"
-
- # Add inline conflict warning
- if inline_conflict and warning:
- entry += f"⚠️ **Conflict**: {warning}\n\n"
-
- # Show both versions if available
- conflict = api_data.get('conflict', {})
- if conflict:
- docs_info = conflict.get('docs_info')
- code_info = conflict.get('code_info')
-
- if docs_info and code_info:
- entry += "**Documentation says:**\n"
- entry += f"```\n{docs_info.get('raw_signature', 'N/A')}\n```\n\n"
- entry += "**Code implementation:**\n"
- entry += f"```\n{self._format_code_signature(code_info)}\n```\n\n"
-
- # Add source info
- source = api_data.get('source', 'unknown')
- entry += f"*Source: {source}*\n\n"
-
- entry += "---\n\n"
-
- return entry
-
- def _format_code_signature(self, code_info: Dict) -> str:
- """Format code signature for display."""
- name = code_info.get('name', '')
- params = code_info.get('parameters', [])
- return_type = code_info.get('return_type')
-
- param_strs = []
- for param in params:
- param_str = param.get('name', '')
- if param.get('type_hint'):
- param_str += f": {param['type_hint']}"
- if param.get('default'):
- param_str += f" = {param['default']}"
- param_strs.append(param_str)
-
- sig = f"{name}({', '.join(param_strs)})"
- if return_type:
- sig += f" -> {return_type}"
-
- return sig
-
- def _generate_references(self):
- """Generate reference files organized by source."""
- logger.info("Generating reference files...")
-
- # Generate references for each source type
- if 'documentation' in self.scraped_data:
- self._generate_docs_references()
-
- if 'github' in self.scraped_data:
- self._generate_github_references()
-
- if 'pdf' in self.scraped_data:
- self._generate_pdf_references()
-
- # Generate merged API reference if available
- if self.merged_data:
- self._generate_merged_api_reference()
-
- def _generate_docs_references(self):
- """Generate references from documentation source."""
- docs_dir = os.path.join(self.skill_dir, 'references', 'documentation')
- os.makedirs(docs_dir, exist_ok=True)
-
- # Create index
- index_path = os.path.join(docs_dir, 'index.md')
- with open(index_path, 'w') as f:
- f.write("# Documentation\n\n")
- f.write("Reference from official documentation.\n\n")
-
- logger.info("Created documentation references")
-
- def _generate_github_references(self):
- """Generate references from GitHub source."""
- github_dir = os.path.join(self.skill_dir, 'references', 'github')
- os.makedirs(github_dir, exist_ok=True)
-
- github_data = self.scraped_data['github']['data']
-
- # Create README reference
- if github_data.get('readme'):
- readme_path = os.path.join(github_dir, 'README.md')
- with open(readme_path, 'w') as f:
- f.write("# Repository README\n\n")
- f.write(github_data['readme'])
-
- # Create issues reference
- if github_data.get('issues'):
- issues_path = os.path.join(github_dir, 'issues.md')
- with open(issues_path, 'w') as f:
- f.write("# GitHub Issues\n\n")
- f.write(f"{len(github_data['issues'])} recent issues.\n\n")
-
- for issue in github_data['issues'][:20]:
- f.write(f"## #{issue['number']}: {issue['title']}\n\n")
- f.write(f"**State**: {issue['state']}\n")
- if issue.get('labels'):
- f.write(f"**Labels**: {', '.join(issue['labels'])}\n")
- f.write(f"**URL**: {issue.get('url', 'N/A')}\n\n")
-
- # Create releases reference
- if github_data.get('releases'):
- releases_path = os.path.join(github_dir, 'releases.md')
- with open(releases_path, 'w') as f:
- f.write("# Releases\n\n")
-
- for release in github_data['releases'][:10]:
- f.write(f"## {release['tag_name']}: {release.get('name', 'N/A')}\n\n")
- f.write(f"**Published**: {release.get('published_at', 'N/A')[:10]}\n\n")
- if release.get('body'):
- f.write(release['body'][:500])
- f.write("\n\n")
-
- logger.info("Created GitHub references")
-
- def _generate_pdf_references(self):
- """Generate references from PDF source."""
- pdf_dir = os.path.join(self.skill_dir, 'references', 'pdf')
- os.makedirs(pdf_dir, exist_ok=True)
-
- # Create index
- index_path = os.path.join(pdf_dir, 'index.md')
- with open(index_path, 'w') as f:
- f.write("# PDF Documentation\n\n")
- f.write("Reference from PDF document.\n\n")
-
- logger.info("Created PDF references")
-
- def _generate_merged_api_reference(self):
- """Generate merged API reference file."""
- api_dir = os.path.join(self.skill_dir, 'references', 'api')
- os.makedirs(api_dir, exist_ok=True)
-
- api_path = os.path.join(api_dir, 'merged_api.md')
-
- with open(api_path, 'w') as f:
- f.write("# Merged API Reference\n\n")
- f.write("*Combined from documentation and code analysis*\n\n")
-
- apis = self.merged_data.get('apis', {})
-
- for api_name in sorted(apis.keys()):
- api_data = apis[api_name]
- entry = self._format_api_entry(api_data, inline_conflict=True)
- f.write(entry)
-
- logger.info(f"Created merged API reference ({len(apis)} APIs)")
-
- def _generate_conflicts_report(self):
- """Generate detailed conflicts report."""
- conflicts_path = os.path.join(self.skill_dir, 'references', 'conflicts.md')
-
- with open(conflicts_path, 'w') as f:
- f.write("# Conflict Report\n\n")
- f.write(f"Found **{len(self.conflicts)}** conflicts between sources.\n\n")
-
- # Group by severity
- high = [c for c in self.conflicts if (hasattr(c, 'severity') and c.severity == 'high') or c.get('severity') == 'high']
- medium = [c for c in self.conflicts if (hasattr(c, 'severity') and c.severity == 'medium') or c.get('severity') == 'medium']
- low = [c for c in self.conflicts if (hasattr(c, 'severity') and c.severity == 'low') or c.get('severity') == 'low']
-
- f.write("## Severity Breakdown\n\n")
- f.write(f"- 🔴 **High**: {len(high)} (action required)\n")
- f.write(f"- 🟡 **Medium**: {len(medium)} (review recommended)\n")
- f.write(f"- 🟢 **Low**: {len(low)} (informational)\n\n")
-
- # List high severity conflicts
- if high:
- f.write("## 🔴 High Severity\n\n")
- f.write("*These conflicts require immediate attention*\n\n")
-
- for conflict in high:
- api_name = conflict.api_name if hasattr(conflict, 'api_name') else conflict.get('api_name', 'Unknown')
- diff = conflict.difference if hasattr(conflict, 'difference') else conflict.get('difference', 'N/A')
-
- f.write(f"### {api_name}\n\n")
- f.write(f"**Issue**: {diff}\n\n")
-
- # List medium severity
- if medium:
- f.write("## 🟡 Medium Severity\n\n")
-
- for conflict in medium[:20]: # Limit to 20
- api_name = conflict.api_name if hasattr(conflict, 'api_name') else conflict.get('api_name', 'Unknown')
- diff = conflict.difference if hasattr(conflict, 'difference') else conflict.get('difference', 'N/A')
-
- f.write(f"### {api_name}\n\n")
- f.write(f"{diff}\n\n")
-
- logger.info(f"Created conflicts report")
-
-
-if __name__ == '__main__':
- # Test with mock data
- import sys
-
- if len(sys.argv) < 2:
- print("Usage: python unified_skill_builder.py ")
- sys.exit(1)
-
- config_path = sys.argv[1]
-
- with open(config_path, 'r') as f:
- config = json.load(f)
-
- # Mock scraped data
- scraped_data = {
- 'github': {
- 'data': {
- 'readme': '# Test Repository',
- 'issues': [],
- 'releases': []
- }
- }
- }
-
- builder = UnifiedSkillBuilder(config, scraped_data)
- builder.build()
-
- print(f"\n✅ Test skill built in: output/{config['name']}/")
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/upload_skill.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/upload_skill.py
deleted file mode 100644
index 0694195..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/upload_skill.py
+++ /dev/null
@@ -1,175 +0,0 @@
-#!/usr/bin/env python3
-"""
-Automatic Skill Uploader
-Uploads a skill .zip file to Claude using the Anthropic API
-
-Usage:
- # Set API key (one-time)
- export ANTHROPIC_API_KEY=sk-ant-...
-
- # Upload skill
- python3 upload_skill.py output/react.zip
- python3 upload_skill.py output/godot.zip
-"""
-
-import os
-import sys
-import json
-import argparse
-from pathlib import Path
-
-# Import utilities
-try:
- from utils import (
- get_api_key,
- get_upload_url,
- print_upload_instructions,
- validate_zip_file
- )
-except ImportError:
- sys.path.insert(0, str(Path(__file__).parent))
- from utils import (
- get_api_key,
- get_upload_url,
- print_upload_instructions,
- validate_zip_file
- )
-
-
-def upload_skill_api(zip_path):
- """
- Upload skill to Claude via Anthropic API
-
- Args:
- zip_path: Path to skill .zip file
-
- Returns:
- tuple: (success, message)
- """
- # Check for requests library
- try:
- import requests
- except ImportError:
- return False, "requests library not installed. Run: pip install requests"
-
- # Validate zip file
- is_valid, error_msg = validate_zip_file(zip_path)
- if not is_valid:
- return False, error_msg
-
- # Get API key
- api_key = get_api_key()
- if not api_key:
- return False, "ANTHROPIC_API_KEY not set. Run: export ANTHROPIC_API_KEY=sk-ant-..."
-
- zip_path = Path(zip_path)
- skill_name = zip_path.stem
-
- print(f"📤 Uploading skill: {skill_name}")
- print(f" Source: {zip_path}")
- print(f" Size: {zip_path.stat().st_size:,} bytes")
- print()
-
- # Prepare API request
- api_url = "https://api.anthropic.com/v1/skills"
- headers = {
- "x-api-key": api_key,
- "anthropic-version": "2023-06-01",
- "anthropic-beta": "skills-2025-10-02"
- }
-
- try:
- # Read zip file
- with open(zip_path, 'rb') as f:
- zip_data = f.read()
-
- # Upload skill
- print("⏳ Uploading to Anthropic API...")
-
- files = {
- 'files[]': (zip_path.name, zip_data, 'application/zip')
- }
-
- response = requests.post(
- api_url,
- headers=headers,
- files=files,
- timeout=60
- )
-
- # Check response
- if response.status_code == 200:
- print()
- print("✅ Skill uploaded successfully!")
- print()
- print("Your skill is now available in Claude at:")
- print(f" {get_upload_url()}")
- print()
- return True, "Upload successful"
-
- elif response.status_code == 401:
- return False, "Authentication failed. Check your ANTHROPIC_API_KEY"
-
- elif response.status_code == 400:
- error_msg = response.json().get('error', {}).get('message', 'Unknown error')
- return False, f"Invalid skill format: {error_msg}"
-
- else:
- error_msg = response.json().get('error', {}).get('message', 'Unknown error')
- return False, f"Upload failed ({response.status_code}): {error_msg}"
-
- except requests.exceptions.Timeout:
- return False, "Upload timed out. Try again or use manual upload"
-
- except requests.exceptions.ConnectionError:
- return False, "Connection error. Check your internet connection"
-
- except Exception as e:
- return False, f"Unexpected error: {str(e)}"
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Upload a skill .zip file to Claude via Anthropic API",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Setup:
- 1. Get your Anthropic API key from https://console.anthropic.com/
- 2. Set the API key:
- export ANTHROPIC_API_KEY=sk-ant-...
-
-Examples:
- # Upload skill
- python3 upload_skill.py output/react.zip
-
- # Upload with explicit path
- python3 upload_skill.py /path/to/skill.zip
-
-Requirements:
- - ANTHROPIC_API_KEY environment variable must be set
- - requests library (pip install requests)
- """
- )
-
- parser.add_argument(
- 'zip_file',
- help='Path to skill .zip file (e.g., output/react.zip)'
- )
-
- args = parser.parse_args()
-
- # Upload skill
- success, message = upload_skill_api(args.zip_file)
-
- if success:
- sys.exit(0)
- else:
- print(f"\n❌ Upload failed: {message}")
- print()
- print("📝 Manual upload instructions:")
- print_upload_instructions(args.zip_file)
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/utils.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/utils.py
deleted file mode 100644
index 2432cd1..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/cli/utils.py
+++ /dev/null
@@ -1,224 +0,0 @@
-#!/usr/bin/env python3
-"""
-Utility functions for Skill Seeker CLI tools
-"""
-
-import os
-import sys
-import subprocess
-import platform
-from pathlib import Path
-from typing import Optional, Tuple, Dict, Union
-
-
-def open_folder(folder_path: Union[str, Path]) -> bool:
- """
- Open a folder in the system file browser
-
- Args:
- folder_path: Path to folder to open
-
- Returns:
- bool: True if successful, False otherwise
- """
- folder_path = Path(folder_path).resolve()
-
- if not folder_path.exists():
- print(f"⚠️ Folder not found: {folder_path}")
- return False
-
- system = platform.system()
-
- try:
- if system == "Linux":
- # Try xdg-open first (standard)
- subprocess.run(["xdg-open", str(folder_path)], check=True)
- elif system == "Darwin": # macOS
- subprocess.run(["open", str(folder_path)], check=True)
- elif system == "Windows":
- subprocess.run(["explorer", str(folder_path)], check=True)
- else:
- print(f"⚠️ Unknown operating system: {system}")
- return False
-
- return True
-
- except subprocess.CalledProcessError:
- print(f"⚠️ Could not open folder automatically")
- return False
- except FileNotFoundError:
- print(f"⚠️ File browser not found on system")
- return False
-
-
-def has_api_key() -> bool:
- """
- Check if ANTHROPIC_API_KEY is set in environment
-
- Returns:
- bool: True if API key is set, False otherwise
- """
- api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
- return len(api_key) > 0
-
-
-def get_api_key() -> Optional[str]:
- """
- Get ANTHROPIC_API_KEY from environment
-
- Returns:
- str: API key or None if not set
- """
- api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
- return api_key if api_key else None
-
-
-def get_upload_url() -> str:
- """
- Get the Claude skills upload URL
-
- Returns:
- str: Claude skills upload URL
- """
- return "https://claude.ai/skills"
-
-
-def print_upload_instructions(zip_path: Union[str, Path]) -> None:
- """
- Print clear upload instructions for manual upload
-
- Args:
- zip_path: Path to the .zip file to upload
- """
- zip_path = Path(zip_path)
-
- print()
- print("╔══════════════════════════════════════════════════════════╗")
- print("║ NEXT STEP ║")
- print("╚══════════════════════════════════════════════════════════╝")
- print()
- print(f"📤 Upload to Claude: {get_upload_url()}")
- print()
- print(f"1. Go to {get_upload_url()}")
- print("2. Click \"Upload Skill\"")
- print(f"3. Select: {zip_path}")
- print("4. Done! ✅")
- print()
-
-
-def format_file_size(size_bytes: int) -> str:
- """
- Format file size in human-readable format
-
- Args:
- size_bytes: Size in bytes
-
- Returns:
- str: Formatted size (e.g., "45.3 KB")
- """
- if size_bytes < 1024:
- return f"{size_bytes} bytes"
- elif size_bytes < 1024 * 1024:
- return f"{size_bytes / 1024:.1f} KB"
- else:
- return f"{size_bytes / (1024 * 1024):.1f} MB"
-
-
-def validate_skill_directory(skill_dir: Union[str, Path]) -> Tuple[bool, Optional[str]]:
- """
- Validate that a directory is a valid skill directory
-
- Args:
- skill_dir: Path to skill directory
-
- Returns:
- tuple: (is_valid, error_message)
- """
- skill_path = Path(skill_dir)
-
- if not skill_path.exists():
- return False, f"Directory not found: {skill_dir}"
-
- if not skill_path.is_dir():
- return False, f"Not a directory: {skill_dir}"
-
- skill_md = skill_path / "SKILL.md"
- if not skill_md.exists():
- return False, f"SKILL.md not found in {skill_dir}"
-
- return True, None
-
-
-def validate_zip_file(zip_path: Union[str, Path]) -> Tuple[bool, Optional[str]]:
- """
- Validate that a file is a valid skill .zip file
-
- Args:
- zip_path: Path to .zip file
-
- Returns:
- tuple: (is_valid, error_message)
- """
- zip_path = Path(zip_path)
-
- if not zip_path.exists():
- return False, f"File not found: {zip_path}"
-
- if not zip_path.is_file():
- return False, f"Not a file: {zip_path}"
-
- if not zip_path.suffix == '.zip':
- return False, f"Not a .zip file: {zip_path}"
-
- return True, None
-
-
-def read_reference_files(skill_dir: Union[str, Path], max_chars: int = 100000, preview_limit: int = 40000) -> Dict[str, str]:
- """Read reference files from a skill directory with size limits.
-
- This function reads markdown files from the references/ subdirectory
- of a skill, applying both per-file and total content limits.
-
- Args:
- skill_dir (str or Path): Path to skill directory
- max_chars (int): Maximum total characters to read (default: 100000)
- preview_limit (int): Maximum characters per file (default: 40000)
-
- Returns:
- dict: Dictionary mapping filename to content
-
- Example:
- >>> refs = read_reference_files('output/react/', max_chars=50000)
- >>> len(refs)
- 5
- """
- from pathlib import Path
-
- skill_path = Path(skill_dir)
- references_dir = skill_path / "references"
- references: Dict[str, str] = {}
-
- if not references_dir.exists():
- print(f"⚠ No references directory found at {references_dir}")
- return references
-
- total_chars = 0
- for ref_file in sorted(references_dir.glob("*.md")):
- if ref_file.name == "index.md":
- continue
-
- content = ref_file.read_text(encoding='utf-8')
-
- # Limit size per file
- if len(content) > preview_limit:
- content = content[:preview_limit] + "\n\n[Content truncated...]"
-
- references[ref_file.name] = content
- total_chars += len(content)
-
- # Stop if we've read enough
- if total_chars > max_chars:
- print(f" ℹ Limiting input to {max_chars:,} characters")
- break
-
- return references
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/__init__.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/__init__.py
deleted file mode 100644
index 4616b37..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Skill Seekers MCP (Model Context Protocol) server package.
-
-This package provides MCP server integration for Claude Code, allowing
-natural language interaction with Skill Seekers tools.
-
-Main modules:
- - server: MCP server implementation with 9 tools
-
-Available MCP Tools:
- - list_configs: List all available preset configurations
- - generate_config: Generate a new config file for any docs site
- - validate_config: Validate a config file structure
- - estimate_pages: Estimate page count before scraping
- - scrape_docs: Scrape and build a skill
- - package_skill: Package skill into .zip file (with auto-upload)
- - upload_skill: Upload .zip to Claude
- - split_config: Split large documentation configs
- - generate_router: Generate router/hub skills
-
-Usage:
- The MCP server is typically run by Claude Code via configuration
- in ~/.config/claude-code/mcp.json
-"""
-
-__version__ = "2.0.0"
-
-__all__ = []
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/requirements.txt b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/requirements.txt
deleted file mode 100644
index 18088ef..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# MCP Server dependencies
-mcp>=1.0.0
-
-# CLI tool dependencies (shared)
-requests>=2.31.0
-beautifulsoup4>=4.12.0
-
-# Optional: for API-based enhancement
-# anthropic>=0.18.0
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/server.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/server.py
deleted file mode 100644
index 4e054de..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/server.py
+++ /dev/null
@@ -1,1064 +0,0 @@
-#!/usr/bin/env python3
-"""
-Skill Seeker MCP Server
-Model Context Protocol server for generating Claude AI skills from documentation
-"""
-
-import asyncio
-import json
-import os
-import subprocess
-import sys
-import time
-from pathlib import Path
-from typing import Any
-
-# Import external MCP package
-# NOTE: Directory renamed from 'mcp/' to 'skill_seeker_mcp/' to avoid shadowing the external mcp package
-MCP_AVAILABLE = False
-Server = None
-Tool = None
-TextContent = None
-
-try:
- from mcp.server import Server
- from mcp.types import Tool, TextContent
- MCP_AVAILABLE = True
-except ImportError as e:
- if __name__ == "__main__":
- print("❌ Error: mcp package not installed")
- print("Install with: pip install mcp")
- print(f"Import error: {e}")
- sys.exit(1)
-
-
-# Initialize MCP server (only if MCP is available)
-app = Server("skill-seeker") if MCP_AVAILABLE and Server is not None else None
-
-# Path to CLI tools
-CLI_DIR = Path(__file__).parent.parent / "cli"
-
-# Helper decorator that works even when app is None
-def safe_decorator(decorator_func):
- """Returns the decorator if MCP is available, otherwise returns a no-op"""
- if MCP_AVAILABLE and app is not None:
- return decorator_func
- else:
- # Return a decorator that just returns the function unchanged
- def noop_decorator(func):
- return func
- return noop_decorator
-
-
-def run_subprocess_with_streaming(cmd, timeout=None):
- """
- Run subprocess with real-time output streaming.
- Returns (stdout, stderr, returncode).
-
- This solves the blocking issue where long-running processes (like scraping)
- would cause MCP to appear frozen. Now we stream output as it comes.
- """
- try:
- process = subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- bufsize=1, # Line buffered
- universal_newlines=True
- )
-
- stdout_lines = []
- stderr_lines = []
- start_time = time.time()
-
- # Read output line by line as it comes
- while True:
- # Check timeout
- if timeout and (time.time() - start_time) > timeout:
- process.kill()
- stderr_lines.append(f"\n⚠️ Process killed after {timeout}s timeout")
- break
-
- # Check if process finished
- if process.poll() is not None:
- break
-
- # Read available output (non-blocking)
- try:
- import select
- readable, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
-
- if process.stdout in readable:
- line = process.stdout.readline()
- if line:
- stdout_lines.append(line)
-
- if process.stderr in readable:
- line = process.stderr.readline()
- if line:
- stderr_lines.append(line)
- except:
- # Fallback for Windows (no select)
- time.sleep(0.1)
-
- # Get any remaining output
- remaining_stdout, remaining_stderr = process.communicate()
- if remaining_stdout:
- stdout_lines.append(remaining_stdout)
- if remaining_stderr:
- stderr_lines.append(remaining_stderr)
-
- stdout = ''.join(stdout_lines)
- stderr = ''.join(stderr_lines)
- returncode = process.returncode
-
- return stdout, stderr, returncode
-
- except Exception as e:
- return "", f"Error running subprocess: {str(e)}", 1
-
-
-@safe_decorator(app.list_tools() if app else lambda: lambda f: f)
-async def list_tools() -> list[Tool]:
- """List available tools"""
- return [
- Tool(
- name="generate_config",
- description="Generate a config file for documentation scraping. Interactively creates a JSON config for any documentation website.",
- inputSchema={
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Skill name (lowercase, alphanumeric, hyphens, underscores)",
- },
- "url": {
- "type": "string",
- "description": "Base documentation URL (must include http:// or https://)",
- },
- "description": {
- "type": "string",
- "description": "Description of when to use this skill",
- },
- "max_pages": {
- "type": "integer",
- "description": "Maximum pages to scrape (default: 100, use -1 for unlimited)",
- "default": 100,
- },
- "unlimited": {
- "type": "boolean",
- "description": "Remove all limits - scrape all pages (default: false). Overrides max_pages.",
- "default": False,
- },
- "rate_limit": {
- "type": "number",
- "description": "Delay between requests in seconds (default: 0.5)",
- "default": 0.5,
- },
- },
- "required": ["name", "url", "description"],
- },
- ),
- Tool(
- name="estimate_pages",
- description="Estimate how many pages will be scraped from a config. Fast preview without downloading content.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to config JSON file (e.g., configs/react.json)",
- },
- "max_discovery": {
- "type": "integer",
- "description": "Maximum pages to discover during estimation (default: 1000, use -1 for unlimited)",
- "default": 1000,
- },
- "unlimited": {
- "type": "boolean",
- "description": "Remove discovery limit - estimate all pages (default: false). Overrides max_discovery.",
- "default": False,
- },
- },
- "required": ["config_path"],
- },
- ),
- Tool(
- name="scrape_docs",
- description="Scrape documentation and build Claude skill. Supports both single-source (legacy) and unified multi-source configs. Creates SKILL.md and reference files. Automatically detects llms.txt files for 10x faster processing. Falls back to HTML scraping if not available.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to config JSON file (e.g., configs/react.json or configs/godot_unified.json)",
- },
- "unlimited": {
- "type": "boolean",
- "description": "Remove page limit - scrape all pages (default: false). Overrides max_pages in config.",
- "default": False,
- },
- "enhance_local": {
- "type": "boolean",
- "description": "Open terminal for local enhancement with Claude Code (default: false)",
- "default": False,
- },
- "skip_scrape": {
- "type": "boolean",
- "description": "Skip scraping, use cached data (default: false)",
- "default": False,
- },
- "dry_run": {
- "type": "boolean",
- "description": "Preview what will be scraped without saving (default: false)",
- "default": False,
- },
- "merge_mode": {
- "type": "string",
- "description": "Override merge mode for unified configs: 'rule-based' or 'claude-enhanced' (default: from config)",
- },
- },
- "required": ["config_path"],
- },
- ),
- Tool(
- name="package_skill",
- description="Package a skill directory into a .zip file ready for Claude upload. Automatically uploads if ANTHROPIC_API_KEY is set.",
- inputSchema={
- "type": "object",
- "properties": {
- "skill_dir": {
- "type": "string",
- "description": "Path to skill directory (e.g., output/react/)",
- },
- "auto_upload": {
- "type": "boolean",
- "description": "Try to upload automatically if API key is available (default: true). If false, only package without upload attempt.",
- "default": True,
- },
- },
- "required": ["skill_dir"],
- },
- ),
- Tool(
- name="upload_skill",
- description="Upload a skill .zip file to Claude automatically (requires ANTHROPIC_API_KEY)",
- inputSchema={
- "type": "object",
- "properties": {
- "skill_zip": {
- "type": "string",
- "description": "Path to skill .zip file (e.g., output/react.zip)",
- },
- },
- "required": ["skill_zip"],
- },
- ),
- Tool(
- name="list_configs",
- description="List all available preset configurations.",
- inputSchema={
- "type": "object",
- "properties": {},
- },
- ),
- Tool(
- name="validate_config",
- description="Validate a config file for errors.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to config JSON file",
- },
- },
- "required": ["config_path"],
- },
- ),
- Tool(
- name="split_config",
- description="Split large documentation config into multiple focused skills. For 10K+ page documentation.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to config JSON file (e.g., configs/godot.json)",
- },
- "strategy": {
- "type": "string",
- "description": "Split strategy: auto, none, category, router, size (default: auto)",
- "default": "auto",
- },
- "target_pages": {
- "type": "integer",
- "description": "Target pages per skill (default: 5000)",
- "default": 5000,
- },
- "dry_run": {
- "type": "boolean",
- "description": "Preview without saving files (default: false)",
- "default": False,
- },
- },
- "required": ["config_path"],
- },
- ),
- Tool(
- name="generate_router",
- description="Generate router/hub skill for split documentation. Creates intelligent routing to sub-skills.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_pattern": {
- "type": "string",
- "description": "Config pattern for sub-skills (e.g., 'configs/godot-*.json')",
- },
- "router_name": {
- "type": "string",
- "description": "Router skill name (optional, inferred from configs)",
- },
- },
- "required": ["config_pattern"],
- },
- ),
- Tool(
- name="scrape_pdf",
- description="Scrape PDF documentation and build Claude skill. Extracts text, code, and images from PDF files.",
- inputSchema={
- "type": "object",
- "properties": {
- "config_path": {
- "type": "string",
- "description": "Path to PDF config JSON file (e.g., configs/manual_pdf.json)",
- },
- "pdf_path": {
- "type": "string",
- "description": "Direct PDF path (alternative to config_path)",
- },
- "name": {
- "type": "string",
- "description": "Skill name (required with pdf_path)",
- },
- "description": {
- "type": "string",
- "description": "Skill description (optional)",
- },
- "from_json": {
- "type": "string",
- "description": "Build from extracted JSON file (e.g., output/manual_extracted.json)",
- },
- },
- "required": [],
- },
- ),
- Tool(
- name="scrape_github",
- description="Scrape GitHub repository and build Claude skill. Extracts README, Issues, Changelog, Releases, and code structure.",
- inputSchema={
- "type": "object",
- "properties": {
- "repo": {
- "type": "string",
- "description": "GitHub repository (owner/repo, e.g., facebook/react)",
- },
- "config_path": {
- "type": "string",
- "description": "Path to GitHub config JSON file (e.g., configs/react_github.json)",
- },
- "name": {
- "type": "string",
- "description": "Skill name (default: repo name)",
- },
- "description": {
- "type": "string",
- "description": "Skill description",
- },
- "token": {
- "type": "string",
- "description": "GitHub personal access token (or use GITHUB_TOKEN env var)",
- },
- "no_issues": {
- "type": "boolean",
- "description": "Skip GitHub issues extraction (default: false)",
- "default": False,
- },
- "no_changelog": {
- "type": "boolean",
- "description": "Skip CHANGELOG extraction (default: false)",
- "default": False,
- },
- "no_releases": {
- "type": "boolean",
- "description": "Skip releases extraction (default: false)",
- "default": False,
- },
- "max_issues": {
- "type": "integer",
- "description": "Maximum issues to fetch (default: 100)",
- "default": 100,
- },
- "scrape_only": {
- "type": "boolean",
- "description": "Only scrape, don't build skill (default: false)",
- "default": False,
- },
- },
- "required": [],
- },
- ),
- ]
-
-
-@safe_decorator(app.call_tool() if app else lambda: lambda f: f)
-async def call_tool(name: str, arguments: Any) -> list[TextContent]:
- """Handle tool calls"""
-
- try:
- if name == "generate_config":
- return await generate_config_tool(arguments)
- elif name == "estimate_pages":
- return await estimate_pages_tool(arguments)
- elif name == "scrape_docs":
- return await scrape_docs_tool(arguments)
- elif name == "package_skill":
- return await package_skill_tool(arguments)
- elif name == "upload_skill":
- return await upload_skill_tool(arguments)
- elif name == "list_configs":
- return await list_configs_tool(arguments)
- elif name == "validate_config":
- return await validate_config_tool(arguments)
- elif name == "split_config":
- return await split_config_tool(arguments)
- elif name == "generate_router":
- return await generate_router_tool(arguments)
- elif name == "scrape_pdf":
- return await scrape_pdf_tool(arguments)
- elif name == "scrape_github":
- return await scrape_github_tool(arguments)
- else:
- return [TextContent(type="text", text=f"Unknown tool: {name}")]
-
- except Exception as e:
- return [TextContent(type="text", text=f"Error: {str(e)}")]
-
-
-async def generate_config_tool(args: dict) -> list[TextContent]:
- """Generate a config file"""
- name = args["name"]
- url = args["url"]
- description = args["description"]
- max_pages = args.get("max_pages", 100)
- unlimited = args.get("unlimited", False)
- rate_limit = args.get("rate_limit", 0.5)
-
- # Handle unlimited mode
- if unlimited:
- max_pages = None
- limit_msg = "unlimited (no page limit)"
- elif max_pages == -1:
- max_pages = None
- limit_msg = "unlimited (no page limit)"
- else:
- limit_msg = str(max_pages)
-
- # Create config
- config = {
- "name": name,
- "description": description,
- "base_url": url,
- "selectors": {
- "main_content": "article",
- "title": "h1",
- "code_blocks": "pre code"
- },
- "url_patterns": {
- "include": [],
- "exclude": []
- },
- "categories": {},
- "rate_limit": rate_limit,
- "max_pages": max_pages
- }
-
- # Save to configs directory
- config_path = Path("configs") / f"{name}.json"
- config_path.parent.mkdir(exist_ok=True)
-
- with open(config_path, 'w') as f:
- json.dump(config, f, indent=2)
-
- result = f"""✅ Config created: {config_path}
-
-Configuration:
- Name: {name}
- URL: {url}
- Max pages: {limit_msg}
- Rate limit: {rate_limit}s
-
-Next steps:
- 1. Review/edit config: cat {config_path}
- 2. Estimate pages: Use estimate_pages tool
- 3. Scrape docs: Use scrape_docs tool
-
-Note: Default selectors may need adjustment for your documentation site.
-"""
-
- return [TextContent(type="text", text=result)]
-
-
-async def estimate_pages_tool(args: dict) -> list[TextContent]:
- """Estimate page count"""
- config_path = args["config_path"]
- max_discovery = args.get("max_discovery", 1000)
- unlimited = args.get("unlimited", False)
-
- # Handle unlimited mode
- if unlimited or max_discovery == -1:
- max_discovery = -1
- timeout = 1800 # 30 minutes for unlimited discovery
- else:
- # Estimate: 0.5s per page discovered
- timeout = max(300, max_discovery // 2) # Minimum 5 minutes
-
- # Run estimate_pages.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "estimate_pages.py"),
- config_path,
- "--max-discovery", str(max_discovery)
- ]
-
- progress_msg = f"🔄 Estimating page count...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def scrape_docs_tool(args: dict) -> list[TextContent]:
- """Scrape documentation - auto-detects unified vs legacy format"""
- config_path = args["config_path"]
- unlimited = args.get("unlimited", False)
- enhance_local = args.get("enhance_local", False)
- skip_scrape = args.get("skip_scrape", False)
- dry_run = args.get("dry_run", False)
- merge_mode = args.get("merge_mode")
-
- # Load config to detect format
- with open(config_path, 'r') as f:
- config = json.load(f)
-
- # Detect if unified format (has 'sources' array)
- is_unified = 'sources' in config and isinstance(config['sources'], list)
-
- # Handle unlimited mode by modifying config temporarily
- if unlimited:
- # Set max_pages to None (unlimited)
- if is_unified:
- # For unified configs, set max_pages on documentation sources
- for source in config.get('sources', []):
- if source.get('type') == 'documentation':
- source['max_pages'] = None
- else:
- # For legacy configs
- config['max_pages'] = None
-
- # Create temporary config file
- temp_config_path = config_path.replace('.json', '_unlimited_temp.json')
- with open(temp_config_path, 'w') as f:
- json.dump(config, f, indent=2)
-
- config_to_use = temp_config_path
- else:
- config_to_use = config_path
-
- # Choose scraper based on format
- if is_unified:
- scraper_script = "unified_scraper.py"
- progress_msg = f"🔄 Starting unified multi-source scraping...\n"
- progress_msg += f"📦 Config format: Unified (multiple sources)\n"
- else:
- scraper_script = "doc_scraper.py"
- progress_msg = f"🔄 Starting scraping process...\n"
- progress_msg += f"📦 Config format: Legacy (single source)\n"
-
- # Build command
- cmd = [
- sys.executable,
- str(CLI_DIR / scraper_script),
- "--config", config_to_use
- ]
-
- # Add merge mode for unified configs
- if is_unified and merge_mode:
- cmd.extend(["--merge-mode", merge_mode])
-
- # Add --fresh to avoid user input prompts when existing data found
- if not skip_scrape:
- cmd.append("--fresh")
-
- if enhance_local:
- cmd.append("--enhance-local")
- if skip_scrape:
- cmd.append("--skip-scrape")
- if dry_run:
- cmd.append("--dry-run")
-
- # Determine timeout based on operation type
- if dry_run:
- timeout = 300 # 5 minutes for dry run
- elif skip_scrape:
- timeout = 600 # 10 minutes for building from cache
- elif unlimited:
- timeout = None # No timeout for unlimited mode (user explicitly requested)
- else:
- # Read config to estimate timeout
- try:
- if is_unified:
- # For unified configs, estimate based on all sources
- total_pages = 0
- for source in config.get('sources', []):
- if source.get('type') == 'documentation':
- total_pages += source.get('max_pages', 500)
- max_pages = total_pages or 500
- else:
- max_pages = config.get('max_pages', 500)
-
- # Estimate: 30s per page + buffer
- timeout = max(3600, max_pages * 35) # Minimum 1 hour, or 35s per page
- except:
- timeout = 14400 # Default: 4 hours
-
- # Add progress message
- if timeout:
- progress_msg += f"⏱️ Maximum time allowed: {timeout // 60} minutes\n"
- else:
- progress_msg += f"⏱️ Unlimited mode - no timeout\n"
- progress_msg += f"📝 Progress will be shown below:\n\n"
-
- # Run scraper with streaming
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- # Clean up temporary config
- if unlimited and Path(config_to_use).exists():
- Path(config_to_use).unlink()
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- error_output = output + f"\n\n❌ Error:\n{stderr}"
- return [TextContent(type="text", text=error_output)]
-
-
-async def package_skill_tool(args: dict) -> list[TextContent]:
- """Package skill to .zip and optionally auto-upload"""
- skill_dir = args["skill_dir"]
- auto_upload = args.get("auto_upload", True)
-
- # Check if API key exists - only upload if available
- has_api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
- should_upload = auto_upload and has_api_key
-
- # Run package_skill.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "package_skill.py"),
- skill_dir,
- "--no-open", # Don't open folder in MCP context
- "--skip-quality-check" # Skip interactive quality checks in MCP context
- ]
-
- # Add upload flag only if we have API key
- if should_upload:
- cmd.append("--upload")
-
- # Timeout: 5 minutes for packaging + upload
- timeout = 300
-
- progress_msg = "📦 Packaging skill...\n"
- if should_upload:
- progress_msg += "📤 Will auto-upload if successful\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- if should_upload:
- # Upload succeeded
- output += "\n\n✅ Skill packaged and uploaded automatically!"
- output += "\n Your skill is now available in Claude!"
- elif auto_upload and not has_api_key:
- # User wanted upload but no API key
- output += "\n\n📝 Skill packaged successfully!"
- output += "\n"
- output += "\n💡 To enable automatic upload:"
- output += "\n 1. Get API key from https://console.anthropic.com/"
- output += "\n 2. Set: export ANTHROPIC_API_KEY=sk-ant-..."
- output += "\n"
- output += "\n📤 Manual upload:"
- output += "\n 1. Find the .zip file in your output/ folder"
- output += "\n 2. Go to https://claude.ai/skills"
- output += "\n 3. Click 'Upload Skill' and select the .zip file"
- else:
- # auto_upload=False, just packaged
- output += "\n\n✅ Skill packaged successfully!"
- output += "\n Upload manually to https://claude.ai/skills"
-
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def upload_skill_tool(args: dict) -> list[TextContent]:
- """Upload skill .zip to Claude"""
- skill_zip = args["skill_zip"]
-
- # Run upload_skill.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "upload_skill.py"),
- skill_zip
- ]
-
- # Timeout: 5 minutes for upload
- timeout = 300
-
- progress_msg = "📤 Uploading skill to Claude...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def list_configs_tool(args: dict) -> list[TextContent]:
- """List available configs"""
- configs_dir = Path("configs")
-
- if not configs_dir.exists():
- return [TextContent(type="text", text="No configs directory found")]
-
- configs = list(configs_dir.glob("*.json"))
-
- if not configs:
- return [TextContent(type="text", text="No config files found")]
-
- result = "📋 Available Configs:\n\n"
-
- for config_file in sorted(configs):
- try:
- with open(config_file) as f:
- config = json.load(f)
- name = config.get("name", config_file.stem)
- desc = config.get("description", "No description")
- url = config.get("base_url", "")
-
- result += f" • {config_file.name}\n"
- result += f" Name: {name}\n"
- result += f" URL: {url}\n"
- result += f" Description: {desc}\n\n"
- except Exception as e:
- result += f" • {config_file.name} - Error reading: {e}\n\n"
-
- return [TextContent(type="text", text=result)]
-
-
-async def validate_config_tool(args: dict) -> list[TextContent]:
- """Validate a config file - supports both legacy and unified formats"""
- config_path = args["config_path"]
-
- # Import validation classes
- sys.path.insert(0, str(CLI_DIR))
-
- try:
- # Check if file exists
- if not Path(config_path).exists():
- return [TextContent(type="text", text=f"❌ Error: Config file not found: {config_path}")]
-
- # Try unified config validator first
- try:
- from config_validator import validate_config
- validator = validate_config(config_path)
-
- result = f"✅ Config is valid!\n\n"
-
- # Show format
- if validator.is_unified:
- result += f"📦 Format: Unified (multi-source)\n"
- result += f" Name: {validator.config['name']}\n"
- result += f" Sources: {len(validator.config.get('sources', []))}\n"
-
- # Show sources
- for i, source in enumerate(validator.config.get('sources', []), 1):
- result += f"\n Source {i}: {source['type']}\n"
- if source['type'] == 'documentation':
- result += f" URL: {source.get('base_url', 'N/A')}\n"
- result += f" Max pages: {source.get('max_pages', 'Not set')}\n"
- elif source['type'] == 'github':
- result += f" Repo: {source.get('repo', 'N/A')}\n"
- result += f" Code depth: {source.get('code_analysis_depth', 'surface')}\n"
- elif source['type'] == 'pdf':
- result += f" Path: {source.get('path', 'N/A')}\n"
-
- # Show merge settings if applicable
- if validator.needs_api_merge():
- merge_mode = validator.config.get('merge_mode', 'rule-based')
- result += f"\n Merge mode: {merge_mode}\n"
- result += f" API merging: Required (docs + code sources)\n"
-
- else:
- result += f"📦 Format: Legacy (single source)\n"
- result += f" Name: {validator.config['name']}\n"
- result += f" Base URL: {validator.config.get('base_url', 'N/A')}\n"
- result += f" Max pages: {validator.config.get('max_pages', 'Not set')}\n"
- result += f" Rate limit: {validator.config.get('rate_limit', 'Not set')}s\n"
-
- return [TextContent(type="text", text=result)]
-
- except ImportError:
- # Fall back to legacy validation
- from doc_scraper import validate_config
- import json
-
- with open(config_path, 'r') as f:
- config = json.load(f)
-
- # Validate config - returns (errors, warnings) tuple
- errors, warnings = validate_config(config)
-
- if errors:
- result = f"❌ Config validation failed:\n\n"
- for error in errors:
- result += f" • {error}\n"
- else:
- result = f"✅ Config is valid!\n\n"
- result += f"📦 Format: Legacy (single source)\n"
- result += f" Name: {config['name']}\n"
- result += f" Base URL: {config['base_url']}\n"
- result += f" Max pages: {config.get('max_pages', 'Not set')}\n"
- result += f" Rate limit: {config.get('rate_limit', 'Not set')}s\n"
-
- if warnings:
- result += f"\n⚠️ Warnings:\n"
- for warning in warnings:
- result += f" • {warning}\n"
-
- return [TextContent(type="text", text=result)]
-
- except Exception as e:
- return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
-
-
-async def split_config_tool(args: dict) -> list[TextContent]:
- """Split large config into multiple focused configs"""
- config_path = args["config_path"]
- strategy = args.get("strategy", "auto")
- target_pages = args.get("target_pages", 5000)
- dry_run = args.get("dry_run", False)
-
- # Run split_config.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "split_config.py"),
- config_path,
- "--strategy", strategy,
- "--target-pages", str(target_pages)
- ]
-
- if dry_run:
- cmd.append("--dry-run")
-
- # Timeout: 5 minutes for config splitting
- timeout = 300
-
- progress_msg = "✂️ Splitting configuration...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def generate_router_tool(args: dict) -> list[TextContent]:
- """Generate router skill for split documentation"""
- import glob
-
- config_pattern = args["config_pattern"]
- router_name = args.get("router_name")
-
- # Expand glob pattern
- config_files = glob.glob(config_pattern)
-
- if not config_files:
- return [TextContent(type="text", text=f"❌ No config files match pattern: {config_pattern}")]
-
- # Run generate_router.py
- cmd = [
- sys.executable,
- str(CLI_DIR / "generate_router.py"),
- ] + config_files
-
- if router_name:
- cmd.extend(["--name", router_name])
-
- # Timeout: 5 minutes for router generation
- timeout = 300
-
- progress_msg = "🧭 Generating router skill...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def scrape_pdf_tool(args: dict) -> list[TextContent]:
- """Scrape PDF documentation and build skill"""
- config_path = args.get("config_path")
- pdf_path = args.get("pdf_path")
- name = args.get("name")
- description = args.get("description")
- from_json = args.get("from_json")
-
- # Build command
- cmd = [sys.executable, str(CLI_DIR / "pdf_scraper.py")]
-
- # Mode 1: Config file
- if config_path:
- cmd.extend(["--config", config_path])
-
- # Mode 2: Direct PDF
- elif pdf_path and name:
- cmd.extend(["--pdf", pdf_path, "--name", name])
- if description:
- cmd.extend(["--description", description])
-
- # Mode 3: From JSON
- elif from_json:
- cmd.extend(["--from-json", from_json])
-
- else:
- return [TextContent(type="text", text="❌ Error: Must specify --config, --pdf + --name, or --from-json")]
-
- # Run pdf_scraper.py with streaming (can take a while)
- timeout = 600 # 10 minutes for PDF extraction
-
- progress_msg = "📄 Scraping PDF documentation...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def scrape_github_tool(args: dict) -> list[TextContent]:
- """Scrape GitHub repository to Claude skill (C1.11)"""
- repo = args.get("repo")
- config_path = args.get("config_path")
- name = args.get("name")
- description = args.get("description")
- token = args.get("token")
- no_issues = args.get("no_issues", False)
- no_changelog = args.get("no_changelog", False)
- no_releases = args.get("no_releases", False)
- max_issues = args.get("max_issues", 100)
- scrape_only = args.get("scrape_only", False)
-
- # Build command
- cmd = [sys.executable, str(CLI_DIR / "github_scraper.py")]
-
- # Mode 1: Config file
- if config_path:
- cmd.extend(["--config", config_path])
-
- # Mode 2: Direct repo
- elif repo:
- cmd.extend(["--repo", repo])
- if name:
- cmd.extend(["--name", name])
- if description:
- cmd.extend(["--description", description])
- if token:
- cmd.extend(["--token", token])
- if no_issues:
- cmd.append("--no-issues")
- if no_changelog:
- cmd.append("--no-changelog")
- if no_releases:
- cmd.append("--no-releases")
- if max_issues != 100:
- cmd.extend(["--max-issues", str(max_issues)])
- if scrape_only:
- cmd.append("--scrape-only")
-
- else:
- return [TextContent(type="text", text="❌ Error: Must specify --repo or --config")]
-
- # Run github_scraper.py with streaming (can take a while)
- timeout = 600 # 10 minutes for GitHub scraping
-
- progress_msg = "🐙 Scraping GitHub repository...\n"
- progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
-
- stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
-
- output = progress_msg + stdout
-
- if returncode == 0:
- return [TextContent(type="text", text=output)]
- else:
- return [TextContent(type="text", text=f"{output}\n\n❌ Error:\n{stderr}")]
-
-
-async def main():
- """Run the MCP server"""
- if not MCP_AVAILABLE or app is None:
- print("❌ Error: MCP server cannot start - MCP package not available")
- sys.exit(1)
-
- from mcp.server.stdio import stdio_server
-
- async with stdio_server() as (read_stream, write_stream):
- await app.run(
- read_stream,
- write_stream,
- app.create_initialization_options()
- )
-
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/tools/__init__.py b/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/tools/__init__.py
deleted file mode 100644
index 388f312..0000000
--- a/assets/skills/skills-skills/scripts/Skill_Seekers-development/src/skill_seekers/mcp/tools/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""MCP tools subpackage.
-
-This package will contain modularized MCP tool implementations.
-
-Planned structure (for future refactoring):
- - scraping_tools.py: Tools for scraping (estimate_pages, scrape_docs)
- - building_tools.py: Tools for building (package_skill, validate_config)
- - deployment_tools.py: Tools for deployment (upload_skill)
- - config_tools.py: Tools for configs (list_configs, generate_config)
- - advanced_tools.py: Advanced tools (split_config, generate_router)
-
-Current state:
- All tools are currently implemented in mcp/server.py
- This directory is a placeholder for future modularization.
-"""
-
-__version__ = "2.0.0"
-
-__all__ = []
diff --git a/assets/skills/snapdom/SKILL.md b/assets/skills/snapdom/SKILL.md
deleted file mode 100644
index 62cfe40..0000000
--- a/assets/skills/snapdom/SKILL.md
+++ /dev/null
@@ -1,274 +0,0 @@
----
-name: snapdom
-description: snapDOM is a fast, accurate DOM-to-image capture tool that converts HTML elements into scalable SVG images. Use for capturing HTML elements, converting DOM to images (SVG, PNG, JPG, WebP), preserving styles, fonts, and pseudo-elements.
----
-
-# SnapDOM Skill
-
-Fast, dependency-free DOM-to-image capture library for converting HTML elements into scalable SVG or raster image formats.
-
-## When to Use This Skill
-
-Use SnapDOM when you need to:
-- Convert HTML elements to images (SVG, PNG, JPG, WebP)
-- Capture styled DOM with pseudo-elements and shadows
-- Export elements with embedded fonts and icons
-- Create screenshots with custom dimensions or scaling
-- Handle CORS-blocked resources using proxy fallback
-- Implement custom rendering pipelines with plugins
-- Optimize performance on large or complex elements
-
-## Key Features
-
-### Universal Export Options
-- **SVG** - Scalable vector format, embeds all styles
-- **PNG, JPG, WebP** - Raster formats with configurable quality
-- **Canvas** - Get raw Canvas element for further processing
-- **Blob** - Raw binary data for custom handling
-
-### Performance
-- Ultra-fast capture (1.6ms for small elements, ~171ms for 4000×2000)
-- **No dependencies** - Uses standard Web APIs only
-- Outperforms html2canvas by 10-40x on complex elements
-
-### Style Support
-- Embedded fonts (including icon fonts)
-- CSS pseudo-elements (::before, ::after)
-- CSS counters
-- CSS line-clamp
-- Transform and shadow effects
-- Shadow DOM content
-
-### Advanced Capabilities
-- Same-origin iframe support
-- CORS proxy fallback for blocked assets
-- Plugin system for custom transformations
-- Straighten transforms (remove rotate/translate)
-- Selective element exclusion
-- Tight bounding box calculation
-
-## Installation
-
-### NPM/Yarn
-```bash
-npm install @zumer/snapdom
-# or
-yarn add @zumer/snapdom
-```
-
-### CDN (ES Module)
-```html
-
-```
-
-### CDN (UMD)
-```html
-
-```
-
-## Quick Start Examples
-
-### Basic Reusable Capture
-```javascript
-// Create reusable capture object
-const result = await snapdom(document.querySelector('#target'));
-
-// Export to different formats
-const png = await result.toPng();
-const jpg = await result.toJpg();
-const svg = await result.toSvg();
-const canvas = await result.toCanvas();
-const blob = await result.toBlob();
-
-// Use the result
-document.body.appendChild(png);
-```
-
-### One-Step Export
-```javascript
-// Direct export without intermediate object
-const png = await snapdom.toPng(document.querySelector('#target'));
-const svg = await snapdom.toSvg(element);
-```
-
-### Download Element
-```javascript
-// Automatically download as file
-await snapdom.download(element, 'screenshot.png');
-await snapdom.download(element, 'image.svg');
-```
-
-### With Options
-```javascript
-const result = await snapdom(element, {
- scale: 2, // 2x resolution
- width: 800, // Custom width
- height: 600, // Custom height
- embedFonts: true, // Include @font-face
- exclude: '.no-capture', // Hide elements
- useProxy: true, // Enable CORS proxy
- straighten: true, // Remove transforms
- noShadows: false // Keep shadows
-});
-
-const png = await result.toPng({ quality: 0.95 });
-```
-
-## Essential Options Reference
-
-| Option | Type | Purpose |
-|--------|------|---------|
-| `scale` | Number | Scale output (e.g., 2 for 2x resolution) |
-| `width` | Number | Custom output width in pixels |
-| `height` | Number | Custom output height in pixels |
-| `embedFonts` | Boolean | Include non-icon @font-face rules |
-| `useProxy` | String\|Boolean | Enable CORS proxy (URL or true for default) |
-| `exclude` | String | CSS selector for elements to hide |
-| `straighten` | Boolean | Remove translate/rotate transforms |
-| `noShadows` | Boolean | Strip shadow effects |
-
-## Common Patterns
-
-### Responsive Screenshots
-```javascript
-// Capture at different scales
-const mobile = await snapdom.toPng(element, { scale: 1 });
-const tablet = await snapdom.toPng(element, { scale: 1.5 });
-const desktop = await snapdom.toPng(element, { scale: 2 });
-```
-
-### Exclude Elements
-```javascript
-// Hide specific elements from capture
-const png = await snapdom.toPng(element, {
- exclude: '.controls, .watermark, [data-no-capture]'
-});
-```
-
-### Fixed Dimensions
-```javascript
-// Capture with specific size
-const result = await snapdom(element, {
- width: 1200,
- height: 630 // Standard social media size
-});
-```
-
-### CORS Handling
-```javascript
-// Fallback for CORS-blocked resources
-const png = await snapdom.toPng(element, {
- useProxy: 'https://cors.example.com/?' // Custom proxy
-});
-```
-
-### Plugin System (Beta)
-```javascript
-// Extend with custom exporters
-snapdom.plugins([pluginFactory, { colorOverlay: true }]);
-
-// Hook into lifecycle
-defineExports(context) {
- return {
- pdf: async (ctx, opts) => { /* generate PDF */ }
- };
-}
-
-// Lifecycle hooks available:
-// beforeSnap → beforeClone → afterClone →
-// beforeRender → beforeExport → afterExport
-```
-
-## Performance Comparison
-
-SnapDOM significantly outperforms html2canvas:
-
-| Scenario | SnapDOM | html2canvas | Improvement |
-|----------|---------|-------------|-------------|
-| Small (200×100) | 1.6ms | 68ms | 42x faster |
-| Medium (800×600) | 12ms | 280ms | 23x faster |
-| Large (4000×2000) | 171ms | 1,800ms | 10x faster |
-
-## Development
-
-### Setup
-```bash
-git clone https://github.com/zumerlab/snapdom.git
-cd snapdom
-npm install
-```
-
-### Build
-```bash
-npm run compile
-```
-
-### Testing
-```bash
-npm test
-```
-
-## Browser Support
-
-- Chrome/Edge 90+
-- Firefox 88+
-- Safari 14+
-- Mobile browsers (iOS Safari 14+, Chrome Mobile)
-
-## Resources
-
-### Documentation
-- **Official Website:** https://snapdom.dev/
-- **GitHub Repository:** https://github.com/zumerlab/snapdom
-- **NPM Package:** https://www.npmjs.com/package/@zumer/snapdom
-- **License:** MIT
-
-### scripts/
-Add helper scripts here for automation, e.g.:
-- `batch-screenshot.js` - Capture multiple elements
-- `pdf-export.js` - Convert snapshots to PDF
-- `compare-outputs.js` - Compare SVG vs PNG quality
-
-### assets/
-Add templates and examples:
-- HTML templates for common capture scenarios
-- CSS frameworks pre-configured with snapdom
-- Boilerplate projects integrating snapdom
-
-## Related Tools
-
-- **html2canvas** - Alternative DOM capture (slower but more compatible)
-- **Orbit CSS Toolkit** - Companion toolkit by Zumerlab (https://github.com/zumerlab/orbit)
-
-## Tips & Best Practices
-
-1. **Performance**: Use `scale` instead of `width`/`height` for better performance
-2. **Fonts**: Set `embedFonts: true` to ensure custom fonts appear correctly
-3. **CORS Issues**: Use `useProxy: true` if images fail to load
-4. **Large Elements**: Break into smaller chunks for complex pages
-5. **Quality**: For PNG/JPG, use `quality: 0.95` for best quality
-6. **SVG Vectors**: Prefer SVG export for charts and graphics
-
-## Troubleshooting
-
-### Elements Not Rendering
-- Check if element has sufficient height/width
-- Verify CSS is fully loaded before capture
-- Try `straighten: false` if transforms are causing issues
-
-### Missing Fonts
-- Set `embedFonts: true`
-- Ensure fonts are loaded before calling snapdom
-- Check browser console for font loading errors
-
-### CORS Issues
-- Enable `useProxy: true`
-- Use custom proxy URL if default fails
-- Check if resources are from same origin
-
-### Performance Issues
-- Reduce `scale` value
-- Use `noShadows: true` to skip shadow rendering
-- Consider splitting large captures into smaller sections
diff --git a/assets/skills/telegram-dev/SKILL.md b/assets/skills/telegram-dev/SKILL.md
deleted file mode 100644
index d064914..0000000
--- a/assets/skills/telegram-dev/SKILL.md
+++ /dev/null
@@ -1,760 +0,0 @@
----
-name: telegram-dev
-description: Telegram 生态开发全栈指南 - 涵盖 Bot API、Mini Apps (Web Apps)、MTProto 客户端开发。包括消息处理、支付、内联模式、Webhook、认证、存储、传感器 API 等完整开发资源。
----
-
-# Telegram 生态开发技能
-
-全面的 Telegram 开发指南,涵盖 Bot 开发、Mini Apps (Web Apps)、客户端开发的完整技术栈。
-
-## 何时使用此技能
-
-当需要以下帮助时使用此技能:
-- 开发 Telegram Bot(消息机器人)
-- 创建 Telegram Mini Apps(小程序)
-- 构建自定义 Telegram 客户端
-- 集成 Telegram 支付和业务功能
-- 实现 Webhook 和长轮询
-- 使用 Telegram 认证和存储
-- 处理消息、媒体和文件
-- 实现内联模式和键盘
-
-## Telegram 开发生态概览
-
-### 三大核心 API
-
-1. **Bot API** - 创建机器人程序
- - HTTP 接口,简单易用
- - 自动处理加密和通信
- - 适合:聊天机器人、自动化工具
-
-2. **Mini Apps API** (Web Apps) - 创建 Web 应用
- - JavaScript 接口
- - 在 Telegram 内运行
- - 适合:小程序、游戏、电商
-
-3. **Telegram API & TDLib** - 创建客户端
- - 完整的 Telegram 协议实现
- - 支持所有平台
- - 适合:自定义客户端、企业应用
-
-## Bot API 开发
-
-### 快速开始
-
-**API 端点:**
-```
-https://api.telegram.org/bot/METHOD_NAME
-```
-
-**获取 Bot Token:**
-1. 与 @BotFather 对话
-2. 发送 `/newbot`
-3. 按提示设置名称
-4. 获取 token
-
-**第一个 Bot (Python):**
-```python
-import requests
-
-BOT_TOKEN = "your_bot_token_here"
-API_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
-
-# 发送消息
-def send_message(chat_id, text):
- url = f"{API_URL}/sendMessage"
- data = {"chat_id": chat_id, "text": text}
- return requests.post(url, json=data)
-
-# 获取更新(长轮询)
-def get_updates(offset=None):
- url = f"{API_URL}/getUpdates"
- params = {"offset": offset, "timeout": 30}
- return requests.get(url, params=params).json()
-
-# 主循环
-offset = None
-while True:
- updates = get_updates(offset)
- for update in updates.get("result", []):
- chat_id = update["message"]["chat"]["id"]
- text = update["message"]["text"]
-
- # 回复消息
- send_message(chat_id, f"你说了:{text}")
-
- offset = update["update_id"] + 1
-```
-
-### 核心 API 方法
-
-**更新管理:**
-- `getUpdates` - 长轮询获取更新
-- `setWebhook` - 设置 Webhook
-- `deleteWebhook` - 删除 Webhook
-- `getWebhookInfo` - 查询 Webhook 状态
-
-**消息操作:**
-- `sendMessage` - 发送文本消息
-- `sendPhoto` / `sendVideo` / `sendDocument` - 发送媒体
-- `sendAudio` / `sendVoice` - 发送音频
-- `sendLocation` / `sendVenue` - 发送位置
-- `editMessageText` - 编辑消息
-- `deleteMessage` - 删除消息
-- `forwardMessage` / `copyMessage` - 转发/复制消息
-
-**交互元素:**
-- `sendPoll` - 发送投票(最多 12 个选项)
-- 内联键盘 (InlineKeyboardMarkup)
-- 回复键盘 (ReplyKeyboardMarkup)
-- `answerCallbackQuery` - 响应回调查询
-
-**文件操作:**
-- `getFile` - 获取文件信息
-- `downloadFile` - 下载文件
-- 支持最大 2GB 文件(本地 Bot API 模式)
-
-**支付功能:**
-- `sendInvoice` - 发送发票
-- `answerPreCheckoutQuery` - 处理支付
-- Telegram Stars 支付(最高 10,000 Stars)
-
-### Webhook 配置
-
-**设置 Webhook:**
-```python
-import requests
-
-BOT_TOKEN = "your_token"
-WEBHOOK_URL = "https://yourdomain.com/webhook"
-
-requests.post(
- f"https://api.telegram.org/bot{BOT_TOKEN}/setWebhook",
- json={"url": WEBHOOK_URL}
-)
-```
-
-**Flask Webhook 示例:**
-```python
-from flask import Flask, request
-import requests
-
-app = Flask(__name__)
-BOT_TOKEN = "your_token"
-
-@app.route('/webhook', methods=['POST'])
-def webhook():
- update = request.get_json()
-
- chat_id = update["message"]["chat"]["id"]
- text = update["message"]["text"]
-
- # 发送回复
- requests.post(
- f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
- json={"chat_id": chat_id, "text": f"收到: {text}"}
- )
-
- return "OK"
-
-if __name__ == '__main__':
- app.run(port=5000)
-```
-
-**Webhook 要求:**
-- 必须使用 HTTPS
-- 支持 TLS 1.2+
-- 端口:443, 80, 88, 8443
-- 公共可访问的 URL
-
-### 内联键盘
-
-**创建内联键盘:**
-```python
-def send_inline_keyboard(chat_id):
- keyboard = {
- "inline_keyboard": [
- [
- {"text": "按钮 1", "callback_data": "btn1"},
- {"text": "按钮 2", "callback_data": "btn2"}
- ],
- [
- {"text": "打开链接", "url": "https://example.com"}
- ]
- ]
- }
-
- requests.post(
- f"{API_URL}/sendMessage",
- json={
- "chat_id": chat_id,
- "text": "选择一个选项:",
- "reply_markup": keyboard
- }
- )
-```
-
-**处理回调:**
-```python
-def handle_callback_query(callback_query):
- query_id = callback_query["id"]
- data = callback_query["data"]
- chat_id = callback_query["message"]["chat"]["id"]
-
- # 响应回调
- requests.post(
- f"{API_URL}/answerCallbackQuery",
- json={"callback_query_id": query_id, "text": f"你点击了 {data}"}
- )
-
- # 更新消息
- requests.post(
- f"{API_URL}/editMessageText",
- json={
- "chat_id": chat_id,
- "message_id": callback_query["message"]["message_id"],
- "text": f"你选择了:{data}"
- }
- )
-```
-
-### 内联模式
-
-**配置内联模式:**
-与 @BotFather 对话,发送 `/setinline`
-
-**处理内联查询:**
-```python
-def handle_inline_query(inline_query):
- query_id = inline_query["id"]
- query_text = inline_query["query"]
-
- # 创建结果
- results = [
- {
- "type": "article",
- "id": "1",
- "title": "结果 1",
- "input_message_content": {
- "message_text": f"你搜索了:{query_text}"
- }
- }
- ]
-
- requests.post(
- f"{API_URL}/answerInlineQuery",
- json={"inline_query_id": query_id, "results": results}
- )
-```
-
-## Mini Apps (Web Apps) 开发
-
-### 初始化 Mini App
-
-**HTML 模板:**
-```html
-
-
-
-
-
-
- My Mini App
-
-
- Telegram Mini App
-
-
-
-
-
-```
-
-### Mini App 核心 API
-
-**WebApp 对象主要属性:**
-```javascript
-// 初始化数据
-tg.initData // 原始初始化字符串
-tg.initDataUnsafe // 解析后的对象
-
-// 用户和主题
-tg.initDataUnsafe.user // 用户信息
-tg.themeParams // 主题颜色
-tg.colorScheme // 'light' 或 'dark'
-
-// 状态
-tg.isExpanded // 是否全屏
-tg.isFullscreen // 是否全屏
-tg.viewportHeight // 视口高度
-tg.platform // 平台类型
-
-// 版本
-tg.version // WebApp 版本
-```
-
-**主要方法:**
-```javascript
-// 窗口控制
-tg.ready() // 标记应用准备就绪
-tg.expand() // 展开到全高度
-tg.close() // 关闭 Mini App
-tg.requestFullscreen() // 请求全屏
-
-// 数据发送
-tg.sendData(data) // 发送数据到 Bot
-
-// 导航
-tg.openLink(url) // 打开外部链接
-tg.openTelegramLink(url) // 打开 Telegram 链接
-
-// 对话框
-tg.showPopup(params, callback) // 显示弹窗
-tg.showAlert(message) // 显示警告
-tg.showConfirm(message) // 显示确认
-
-// 分享
-tg.shareMessage(message) // 分享消息
-tg.shareUrl(url) // 分享链接
-```
-
-### UI 控件
-
-**主按钮 (MainButton):**
-```javascript
-tg.MainButton.setText("点击我");
-tg.MainButton.show();
-tg.MainButton.enable();
-tg.MainButton.showProgress(); // 显示加载
-tg.MainButton.hideProgress();
-
-tg.MainButton.onClick(() => {
- console.log("主按钮被点击");
-});
-```
-
-**次要按钮 (SecondaryButton):**
-```javascript
-tg.SecondaryButton.setText("取消");
-tg.SecondaryButton.show();
-tg.SecondaryButton.onClick(() => {
- tg.close();
-});
-```
-
-**返回按钮 (BackButton):**
-```javascript
-tg.BackButton.show();
-tg.BackButton.onClick(() => {
- // 返回逻辑
-});
-```
-
-**触觉反馈:**
-```javascript
-tg.HapticFeedback.impactOccurred('light'); // light, medium, heavy
-tg.HapticFeedback.notificationOccurred('success'); // success, warning, error
-tg.HapticFeedback.selectionChanged();
-```
-
-### 存储 API
-
-**云存储:**
-```javascript
-// 保存数据
-tg.CloudStorage.setItem('key', 'value', (error, success) => {
- if (success) console.log('保存成功');
-});
-
-// 获取数据
-tg.CloudStorage.getItem('key', (error, value) => {
- console.log('值:', value);
-});
-
-// 删除数据
-tg.CloudStorage.removeItem('key');
-
-// 获取所有键
-tg.CloudStorage.getKeys((error, keys) => {
- console.log('所有键:', keys);
-});
-```
-
-**本地存储:**
-```javascript
-// 普通本地存储
-localStorage.setItem('key', 'value');
-const value = localStorage.getItem('key');
-
-// 安全存储(需要生物识别)
-tg.SecureStorage.setItem('secret', 'value', callback);
-tg.SecureStorage.getItem('secret', callback);
-```
-
-### 生物识别认证
-
-```javascript
-const bioManager = tg.BiometricManager;
-
-// 初始化
-bioManager.init(() => {
- if (bioManager.isInited) {
- console.log('支持的类型:', bioManager.biometricType);
- // 'finger', 'face', 'unknown'
-
- if (bioManager.isAccessGranted) {
- // 已授权,可以使用
- } else {
- // 请求授权
- bioManager.requestAccess({reason: '需要验证身份'}, (success) => {
- if (success) {
- console.log('授权成功');
- }
- });
- }
- }
-});
-
-// 执行认证
-bioManager.authenticate({reason: '确认操作'}, (success, token) => {
- if (success) {
- console.log('认证成功,token:', token);
- }
-});
-```
-
-### 位置和传感器
-
-**获取位置:**
-```javascript
-tg.LocationManager.init(() => {
- if (tg.LocationManager.isInited) {
- tg.LocationManager.getLocation((location) => {
- console.log('纬度:', location.latitude);
- console.log('经度:', location.longitude);
- });
- }
-});
-```
-
-**加速度计:**
-```javascript
-tg.Accelerometer.start({refresh_rate: 100}, (started) => {
- if (started) {
- tg.Accelerometer.onEvent((event) => {
- console.log('加速度:', event.x, event.y, event.z);
- });
- }
-});
-
-// 停止
-tg.Accelerometer.stop();
-```
-
-**陀螺仪:**
-```javascript
-tg.Gyroscope.start({refresh_rate: 100}, callback);
-tg.Gyroscope.onEvent((event) => {
- console.log('旋转速度:', event.x, event.y, event.z);
-});
-```
-
-**设备方向:**
-```javascript
-tg.DeviceOrientation.start({refresh_rate: 100}, callback);
-tg.DeviceOrientation.onEvent((event) => {
- console.log('方向:', event.absolute, event.alpha, event.beta, event.gamma);
-});
-```
-
-### 支付集成
-
-**发起支付 (Telegram Stars):**
-```javascript
-tg.openInvoice('https://t.me/$invoice_link', (status) => {
- if (status === 'paid') {
- console.log('支付成功');
- } else if (status === 'cancelled') {
- console.log('支付取消');
- } else if (status === 'failed') {
- console.log('支付失败');
- }
-});
-```
-
-### 数据验证
-
-**服务器端验证 initData (Python):**
-```python
-import hmac
-import hashlib
-from urllib.parse import parse_qs
-
-def validate_init_data(init_data, bot_token):
- # 解析数据
- parsed = parse_qs(init_data)
- received_hash = parsed.get('hash', [''])[0]
-
- # 移除 hash
- data_check_arr = []
- for key, value in parsed.items():
- if key != 'hash':
- data_check_arr.append(f"{key}={value[0]}")
-
- # 排序
- data_check_arr.sort()
- data_check_string = '\n'.join(data_check_arr)
-
- # 计算密钥
- secret_key = hmac.new(
- b"WebAppData",
- bot_token.encode(),
- hashlib.sha256
- ).digest()
-
- # 计算哈希
- calculated_hash = hmac.new(
- secret_key,
- data_check_string.encode(),
- hashlib.sha256
- ).hexdigest()
-
- return calculated_hash == received_hash
-```
-
-### 启动 Mini App
-
-**从键盘按钮:**
-```python
-keyboard = {
- "keyboard": [[
- {
- "text": "打开应用",
- "web_app": {"url": "https://yourdomain.com/app"}
- }
- ]],
- "resize_keyboard": True
-}
-
-requests.post(
- f"{API_URL}/sendMessage",
- json={
- "chat_id": chat_id,
- "text": "点击按钮打开应用",
- "reply_markup": keyboard
- }
-)
-```
-
-**从内联按钮:**
-```python
-keyboard = {
- "inline_keyboard": [[
- {
- "text": "启动应用",
- "web_app": {"url": "https://yourdomain.com/app"}
- }
- ]]
-}
-```
-
-**从菜单按钮:**
-与 @BotFather 对话:
-```
-/setmenubutton
-→ 选择你的 Bot
-→ 提供 URL: https://yourdomain.com/app
-```
-
-## 客户端开发 (TDLib)
-
-### 使用 TDLib
-
-**Python 示例 (python-telegram):**
-```python
-from telegram.client import Telegram
-
-tg = Telegram(
- api_id='your_api_id',
- api_hash='your_api_hash',
- phone='+1234567890',
- database_encryption_key='changeme1234',
-)
-
-tg.login()
-
-# 发送消息
-result = tg.send_message(
- chat_id=123456789,
- text='Hello from TDLib!'
-)
-
-# 获取聊天列表
-result = tg.get_chats()
-result.wait()
-chats = result.update
-
-print(chats)
-
-tg.stop()
-```
-
-### MTProto 协议
-
-**特点:**
-- 端到端加密
-- 高性能
-- 支持所有 Telegram 功能
-- 需要 API ID/Hash(从 https://my.telegram.org 获取)
-
-## 最佳实践
-
-### Bot 开发
-
-1. **错误处理**
- ```python
- try:
- response = requests.post(url, json=data, timeout=10)
- response.raise_for_status()
- except requests.exceptions.RequestException as e:
- print(f"请求失败: {e}")
- ```
-
-2. **速率限制**
- - 群组消息:最多 20 条/分钟
- - 私聊消息:最多 30 条/秒
- - 全局限制:避免过于频繁
-
-3. **使用 Webhook 而非长轮询**
- - 更高效
- - 更低延迟
- - 更好的可扩展性
-
-4. **数据验证**
- - 始终验证 initData
- - 不要信任客户端数据
- - 服务器端验证所有操作
-
-### Mini Apps 开发
-
-1. **响应式设计**
- ```javascript
- // 监听主题变化
- tg.onEvent('themeChanged', () => {
- document.body.style.backgroundColor = tg.themeParams.bg_color;
- });
-
- // 监听视口变化
- tg.onEvent('viewportChanged', () => {
- console.log('新高度:', tg.viewportHeight);
- });
- ```
-
-2. **性能优化**
- - 最小化 JavaScript 包大小
- - 使用懒加载
- - 优化图片和资源
-
-3. **用户体验**
- - 适配深色/浅色主题
- - 使用原生 UI 控件(MainButton 等)
- - 提供触觉反馈
- - 快速响应用户操作
-
-4. **安全考虑**
- - HTTPS 强制
- - 验证 initData
- - 不在客户端存储敏感信息
- - 使用 SecureStorage 存储密钥
-
-## 常用库和工具
-
-### Python
-- `python-telegram-bot` - 功能强大的 Bot 框架
-- `aiogram` - 异步 Bot 框架
-- `telethon` / `pyrogram` - MTProto 客户端
-
-### Node.js
-- `node-telegram-bot-api` - Bot API 包装器
-- `telegraf` - 现代 Bot 框架
-- `grammy` - 轻量级框架
-
-### 其他语言
-- PHP: `telegram-bot-sdk`
-- Go: `telegram-bot-api`
-- Java: `TelegramBots`
-- C#: `Telegram.Bot`
-
-## 参考资源
-
-### 官方文档
-- Bot API: https://core.telegram.org/bots/api
-- Mini Apps: https://core.telegram.org/bots/webapps
-- Mini Apps Platform: https://docs.telegram-mini-apps.com
-- Telegram API: https://core.telegram.org
-
-### GitHub 仓库
-- Bot API 服务器: https://github.com/tdlib/telegram-bot-api
-- Android 客户端: https://github.com/DrKLO/Telegram
-- Desktop 客户端: https://github.com/telegramdesktop/tdesktop
-- 官方组织: https://github.com/orgs/TelegramOfficial/repositories
-
-### 工具
-- @BotFather - 创建和管理 Bot
-- https://my.telegram.org - 获取 API ID/Hash
-- Telegram Web App 测试环境
-
-## 参考文件
-
-此技能包含详细的 Telegram 开发资源索引和完整实现模板:
-
-- **index.md** - 完整的资源链接和快速导航
-- **Telegram_Bot_按钮和键盘实现模板.md** - 交互式按钮和键盘实现指南(404 行,12 KB)
- - 三种按钮类型详解(Inline/Reply/Command Menu)
- - python-telegram-bot 和 Telethon 双实现对比
- - 完整的即用代码示例和项目结构
- - Handler 系统、错误处理和部署方案
-- **动态视图对齐实现文档.md** - Telegram 数据展示指南(407 行,12 KB)
- - 智能动态对齐算法(三步法,O(n×m) 复杂度)
- - 等宽字体环境的完美对齐方案
- - 智能数值格式化系统(B/M/K 自动缩写)
- - 排行榜和数据表格专业展示
-
-这些精简指南提供了核心的 Telegram Bot 开发解决方案:
-- 按钮和键盘交互的所有实现方式
-- 消息和数据的专业格式化展示
-- 实用的最佳实践和快速参考
-
----
-
-**使用此技能掌握 Telegram 生态的全栈开发!**
diff --git a/assets/skills/timescaledb/SKILL.md b/assets/skills/timescaledb/SKILL.md
deleted file mode 100644
index e6880aa..0000000
--- a/assets/skills/timescaledb/SKILL.md
+++ /dev/null
@@ -1,108 +0,0 @@
----
-name: timescaledb
-description: TimescaleDB - PostgreSQL extension for high-performance time-series and event data analytics, hypertables, continuous aggregates, compression, and real-time analytics
----
-
-# Timescaledb Skill
-
-Comprehensive assistance with timescaledb development, generated from official documentation.
-
-## When to Use This Skill
-
-This skill should be triggered when:
-- Working with timescaledb
-- Asking about timescaledb features or APIs
-- Implementing timescaledb solutions
-- Debugging timescaledb code
-- Learning timescaledb best practices
-
-## Quick Reference
-
-### Common Patterns
-
-*Quick reference patterns will be added as you use the skill.*
-
-### Example Code Patterns
-
-**Example 1** (bash):
-```bash
-rails new my_app -d=postgresql
- cd my_app
-```
-
-**Example 2** (ruby):
-```ruby
-gem 'timescaledb'
-```
-
-**Example 3** (shell):
-```shell
-kubectl create namespace timescale
-```
-
-**Example 4** (shell):
-```shell
-kubectl config set-context --current --namespace=timescale
-```
-
-**Example 5** (sql):
-```sql
-DROP EXTENSION timescaledb;
-```
-
-## Reference Files
-
-This skill includes comprehensive documentation in `references/`:
-
-- **api.md** - Api documentation
-- **compression.md** - Compression documentation
-- **continuous_aggregates.md** - Continuous Aggregates documentation
-- **getting_started.md** - Getting Started documentation
-- **hyperfunctions.md** - Hyperfunctions documentation
-- **hypertables.md** - Hypertables documentation
-- **installation.md** - Installation documentation
-- **other.md** - Other documentation
-- **performance.md** - Performance documentation
-- **time_buckets.md** - Time Buckets documentation
-- **tutorials.md** - Tutorials documentation
-
-Use `view` to read specific reference files when detailed information is needed.
-
-## Working with This Skill
-
-### For Beginners
-Start with the getting_started or tutorials reference files for foundational concepts.
-
-### For Specific Features
-Use the appropriate category reference file (api, guides, etc.) for detailed information.
-
-### For Code Examples
-The quick reference section above contains common patterns extracted from the official docs.
-
-## Resources
-
-### references/
-Organized documentation extracted from official sources. These files contain:
-- Detailed explanations
-- Code examples with language annotations
-- Links to original documentation
-- Table of contents for quick navigation
-
-### scripts/
-Add helper scripts here for common automation tasks.
-
-### assets/
-Add templates, boilerplate, or example projects here.
-
-## Notes
-
-- This skill was automatically generated from official documentation
-- Reference files preserve the structure and examples from source docs
-- Code examples include language detection for better syntax highlighting
-- Quick reference patterns are extracted from common usage examples in the docs
-
-## Updating
-
-To refresh this skill with updated documentation:
-1. Re-run the scraper with the same configuration
-2. The skill will be rebuilt with the latest information
diff --git a/assets/skills/twscrape/SKILL.md b/assets/skills/twscrape/SKILL.md
deleted file mode 100644
index f32d1f4..0000000
--- a/assets/skills/twscrape/SKILL.md
+++ /dev/null
@@ -1,438 +0,0 @@
-# twscrape
-
-Python library for scraping Twitter/X data using GraphQL API with account rotation and session management.
-
-## When to use this skill
-
-Use this skill when:
-- Working with Twitter/X data extraction and scraping
-- Need to bypass Twitter API limitations with account rotation
-- Building social media monitoring or analytics tools
-- Extracting tweets, user profiles, followers, trends from Twitter/X
-- Need async/parallel scraping operations for large-scale data collection
-- Looking for alternatives to official Twitter API
-
-## Quick Reference
-
-### Installation
-
-```bash
-pip install twscrape
-```
-
-### Basic Setup
-
-```python
-import asyncio
-from twscrape import API, gather
-
-async def main():
- api = API() # Uses accounts.db by default
-
- # Add accounts (with cookies - more stable)
- cookies = "abc=12; ct0=xyz"
- await api.pool.add_account("user1", "pass1", "email@example.com", "mail_pass", cookies=cookies)
-
- # Or add accounts (with login/password - less stable)
- await api.pool.add_account("user2", "pass2", "email2@example.com", "mail_pass2")
- await api.pool.login_all()
-
-asyncio.run(main())
-```
-
-### Common Operations
-
-```python
-# Search tweets
-await gather(api.search("elon musk", limit=20))
-
-# Get user info
-await api.user_by_login("xdevelopers")
-user = await api.user_by_id(2244994945)
-
-# Get user tweets
-await gather(api.user_tweets(user_id, limit=20))
-await gather(api.user_tweets_and_replies(user_id, limit=20))
-await gather(api.user_media(user_id, limit=20))
-
-# Get followers/following
-await gather(api.followers(user_id, limit=20))
-await gather(api.following(user_id, limit=20))
-
-# Tweet operations
-await api.tweet_details(tweet_id)
-await gather(api.retweeters(tweet_id, limit=20))
-await gather(api.tweet_replies(tweet_id, limit=20))
-
-# Trends
-await gather(api.trends("news"))
-```
-
-## Key Features
-
-### 1. Multiple API Support
-- **Search API**: Standard Twitter search functionality
-- **GraphQL API**: Advanced queries and data extraction
-- **Automatic switching**: Based on rate limits and availability
-
-### 2. Async/Await Architecture
-```python
-# Parallel scraping
-async for tweet in api.search("elon musk"):
- print(tweet.id, tweet.user.username, tweet.rawContent)
-```
-
-### 3. Account Management
-- Add multiple accounts for rotation
-- Automatic rate limit handling
-- Session persistence across runs
-- Email verification support (IMAP or manual)
-
-### 4. Data Models
-- SNScrape-compatible models
-- Easy conversion to dict/JSON
-- Raw API response access available
-
-## Core API Methods
-
-### Search Operations
-
-#### `search(query, limit, kv={})`
-Search tweets by query string.
-
-**Parameters:**
-- `query` (str): Search query (supports Twitter search syntax)
-- `limit` (int): Maximum number of tweets to return
-- `kv` (dict): Additional parameters (e.g., `{"product": "Top"}` for Top tweets)
-
-**Returns:** AsyncIterator of Tweet objects
-
-**Example:**
-```python
-# Latest tweets
-async for tweet in api.search("elon musk", limit=20):
- print(tweet.rawContent)
-
-# Top tweets
-await gather(api.search("python", limit=20, kv={"product": "Top"}))
-```
-
-### User Operations
-
-#### `user_by_login(username)`
-Get user information by username.
-
-**Example:**
-```python
-user = await api.user_by_login("xdevelopers")
-print(user.id, user.displayname, user.followersCount)
-```
-
-#### `user_by_id(user_id)`
-Get user information by user ID.
-
-#### `followers(user_id, limit)`
-Get user's followers.
-
-#### `following(user_id, limit)`
-Get users that the user follows.
-
-#### `verified_followers(user_id, limit)`
-Get only verified followers.
-
-#### `subscriptions(user_id, limit)`
-Get user's Twitter Blue subscriptions.
-
-### Tweet Operations
-
-#### `tweet_details(tweet_id)`
-Get detailed information about a specific tweet.
-
-#### `tweet_replies(tweet_id, limit)`
-Get replies to a tweet.
-
-#### `retweeters(tweet_id, limit)`
-Get users who retweeted a specific tweet.
-
-#### `user_tweets(user_id, limit)`
-Get tweets from a user (excludes replies).
-
-#### `user_tweets_and_replies(user_id, limit)`
-Get tweets and replies from a user.
-
-#### `user_media(user_id, limit)`
-Get tweets with media from a user.
-
-### Other Operations
-
-#### `list_timeline(list_id)`
-Get tweets from a Twitter list.
-
-#### `trends(category)`
-Get trending topics by category.
-
-**Categories:** "news", "sport", "entertainment", etc.
-
-## Account Management
-
-### Adding Accounts
-
-**With cookies (recommended):**
-```python
-cookies = "abc=12; ct0=xyz" # String or JSON format
-await api.pool.add_account("user", "pass", "email@example.com", "mail_pass", cookies=cookies)
-```
-
-**With credentials:**
-```python
-await api.pool.add_account("user", "pass", "email@example.com", "mail_pass")
-await api.pool.login_all()
-```
-
-### CLI Account Management
-
-```bash
-# Add accounts from file
-twscrape add_accounts accounts.txt username:password:email:email_password
-
-# Login all accounts
-twscrape login_accounts
-
-# Manual email verification
-twscrape login_accounts --manual
-
-# List accounts and status
-twscrape accounts
-
-# Re-login specific accounts
-twscrape relogin user1 user2
-
-# Retry failed logins
-twscrape relogin_failed
-```
-
-## Proxy Configuration
-
-### Per-Account Proxy
-```python
-proxy = "http://login:pass@example.com:8080"
-await api.pool.add_account("user", "pass", "email@example.com", "mail_pass", proxy=proxy)
-```
-
-### Global Proxy
-```python
-api = API(proxy="http://login:pass@example.com:8080")
-```
-
-### Environment Variable
-```bash
-export TWS_PROXY=socks5://user:pass@127.0.0.1:1080
-twscrape search "elon musk"
-```
-
-### Dynamic Proxy Changes
-```python
-api.proxy = "socks5://user:pass@127.0.0.1:1080"
-doc = await api.user_by_login("elonmusk")
-api.proxy = None # Disable proxy
-```
-
-**Priority:** `api.proxy` > `TWS_PROXY` env var > account-specific proxy
-
-## CLI Usage
-
-### Search Operations
-```bash
-twscrape search "QUERY" --limit=20
-twscrape search "elon musk lang:es" --limit=20 > data.txt
-twscrape search "python" --limit=20 --raw # Raw API responses
-```
-
-### User Operations
-```bash
-twscrape user_by_login USERNAME
-twscrape user_by_id USER_ID
-twscrape followers USER_ID --limit=20
-twscrape following USER_ID --limit=20
-twscrape verified_followers USER_ID --limit=20
-twscrape user_tweets USER_ID --limit=20
-```
-
-### Tweet Operations
-```bash
-twscrape tweet_details TWEET_ID
-twscrape tweet_replies TWEET_ID --limit=20
-twscrape retweeters TWEET_ID --limit=20
-```
-
-### Trends
-```bash
-twscrape trends sport
-twscrape trends news
-```
-
-### Custom Database
-```bash
-twscrape --db custom-accounts.db
-```
-
-## Advanced Usage
-
-### Raw API Responses
-```python
-async for response in api.search_raw("elon musk"):
- print(response.status_code, response.json())
-```
-
-### Stopping Iteration
-```python
-from contextlib import aclosing
-
-async with aclosing(api.search("elon musk")) as gen:
- async for tweet in gen:
- if tweet.id < 200:
- break
-```
-
-### Convert Models to Dict/JSON
-```python
-user = await api.user_by_id(user_id)
-user_dict = user.dict()
-user_json = user.json()
-```
-
-### Enable Debug Logging
-```python
-from twscrape.logger import set_log_level
-set_log_level("DEBUG")
-```
-
-## Environment Variables
-
-- **`TWS_PROXY`**: Global proxy for all accounts
- Example: `socks5://user:pass@127.0.0.1:1080`
-
-- **`TWS_WAIT_EMAIL_CODE`**: Timeout for email verification (default: 30 seconds)
-
-- **`TWS_RAISE_WHEN_NO_ACCOUNT`**: Raise exception when no accounts available instead of waiting
- Values: `false`, `0`, `true`, `1` (default: `false`)
-
-## Rate Limits & Limitations
-
-### Rate Limits
-- Rate limits reset **every 15 minutes** per endpoint
-- Each account has **separate limits** for different operations
-- Accounts automatically rotate when limits are reached
-
-### Tweet Limits
-- `user_tweets` and `user_tweets_and_replies` return approximately **3,200 tweets maximum** per user
-- This is a Twitter/X platform limitation
-
-### Account Status
-- Rate limits vary based on:
- - Account age
- - Account verification status
- - Account activity history
-
-### Handling Rate Limits
-The library automatically:
-- Switches to next available account
-- Waits for rate limit reset if all accounts exhausted
-- Tracks rate limit status per endpoint
-
-## Common Patterns
-
-### Large-Scale Data Collection
-```python
-async def collect_user_data(username):
- user = await api.user_by_login(username)
-
- # Collect tweets
- tweets = await gather(api.user_tweets(user.id, limit=100))
-
- # Collect followers
- followers = await gather(api.followers(user.id, limit=100))
-
- # Collect following
- following = await gather(api.following(user.id, limit=100))
-
- return {
- 'user': user,
- 'tweets': tweets,
- 'followers': followers,
- 'following': following
- }
-```
-
-### Search with Filters
-```python
-# Language filter
-await gather(api.search("python lang:en", limit=20))
-
-# Date filter
-await gather(api.search("AI since:2024-01-01", limit=20))
-
-# From specific user
-await gather(api.search("from:elonmusk", limit=20))
-
-# With media
-await gather(api.search("cats filter:media", limit=20))
-```
-
-### Batch Processing
-```python
-async def process_users(usernames):
- tasks = []
- for username in usernames:
- task = api.user_by_login(username)
- tasks.append(task)
-
- users = await asyncio.gather(*tasks)
- return users
-```
-
-## Troubleshooting
-
-### Login Issues
-- **Use cookies instead of credentials** for more stable authentication
-- Enable **manual email verification** with `--manual` flag
-- Check **email password** is correct for IMAP access
-
-### Rate Limit Problems
-- **Add more accounts** for better rotation
-- **Increase wait time** between requests
-- **Monitor account status** with `twscrape accounts`
-
-### No Data Returned
-- **Check account status** - they may be suspended or rate limited
-- **Verify query syntax** - use Twitter search syntax
-- **Try different accounts** - some may have better access
-
-### Connection Issues
-- **Configure proxy** if behind firewall
-- **Check network connectivity**
-- **Verify Twitter/X is accessible** from your location
-
-## Resources
-
-- **GitHub Repository**: https://github.com/vladkens/twscrape
-- **Installation**: `pip install twscrape`
-- **Development Version**: `pip install git+https://github.com/vladkens/twscrape.git`
-
-## References
-
-For detailed API documentation and examples, see the reference files in the `references/` directory:
-
-- `references/installation.md` - Installation and setup
-- `references/api_methods.md` - Complete API method reference
-- `references/account_management.md` - Account configuration and management
-- `references/cli_usage.md` - Command-line interface guide
-- `references/proxy_config.md` - Proxy configuration options
-- `references/examples.md` - Code examples and patterns
-
----
-
-**Repository**: https://github.com/vladkens/twscrape
-**Stars**: 1998+
-**Language**: Python
-**License**: MIT
diff --git a/assets/tasks/0001-repair-ci-lint-and-paths/ACCEPTANCE.md b/assets/tasks/0001-repair-ci-lint-and-paths/ACCEPTANCE.md
deleted file mode 100644
index 68d75d1..0000000
--- a/assets/tasks/0001-repair-ci-lint-and-paths/ACCEPTANCE.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# ACCEPTANCE — 精密验收标准
-
-## 原子断言(Atomic Assertions)
-
-### A1. CI markdownlint 不再硬失败
-
-- Verify:
- - `test -f .github/lint_config.json`
- - `markdownlint --config .github/lint_config.json '**/*.md'`
-- Expected:
- - 不再出现 `Cannot read or parse config file '.github/lint_config.json': ENOENT`
- - 命令退出码为 0
-
-### A2. 本地 `make lint` 与 CI 行为一致
-
-- Verify:
- - `make lint`
- - `markdownlint --config .github/lint_config.json '**/*.md'`
-- Expected:
- - 两者 lint 的覆盖范围一致(至少包含 `assets/documents/**`、`assets/skills/**` 等深层 Markdown)
- - 退出码一致(都为 0)
-
-### A3. 关键入口指引不再引用旧路径(最小集)
-
-- Verify:
- - `rg -n "cp -f config/\\.codex" assets/config/.codex/README.md`
- - `rg -n "\\./skills/skills-skills" assets/skills/skills-skills/references -S`
-- Expected:
- - 上述 grep/rg 均无匹配(或仅在“明确标注为历史示例”的段落中出现,并有解释)
-
-### A4. 忽略规则与新结构一致
-
-- Verify:
- - `rg -n "^assets/repo/backups/gz/" .gitignore`(或等价忽略规则)
- - `git status --porcelain=v1`
-- Expected:
- - `.gitignore` 能覆盖 `assets/repo/backups/gz/`
- - `git status` 不再因为该目录出现未跟踪噪音(除非用户明确想纳入版本控制)
-
-## 边缘路径(Edge Cases,至少 3 个)
-
-1. 在没有启用 `globstar` 的 `/bin/sh` 环境下执行 `make lint` 仍能递归 lint(通过“引用 glob 交给 markdownlint”解决)。
-2. `assets/repo/` 下第三方镜像的 Markdown 仍然存在违规时,lint 策略不会逼迫去改第三方大量文件(通过 `ignorePatterns` 或限定 lint 范围解决,需在 PLAN 明确选择)。
-3. 新增任意 `assets/documents/**.md` 后,`make lint` 必定能扫到(通过新增一个临时 md 文件自测,或用 `markdownlint --debug` 验证匹配)。
-
-## 禁止性准则(Anti-Goals)
-
-- 不以“删除大段内容/关闭整个 lint”来换 CI 绿。
-- 不修改 `.github/workflows/*.yml`(除非证明仅靠配置文件无法修复,且得到明确授权)。
-
diff --git a/assets/tasks/0001-repair-ci-lint-and-paths/CONTEXT.md b/assets/tasks/0001-repair-ci-lint-and-paths/CONTEXT.md
deleted file mode 100644
index daf9aaf..0000000
--- a/assets/tasks/0001-repair-ci-lint-and-paths/CONTEXT.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# CONTEXT — 迁移后 lint/路径问题图谱
-
-## 现状追溯(Live Evidence)
-
-### 1) CI markdownlint 配置缺失(硬失败)
-
-- CI 命令(来自 `.github/workflows/ci.yml`):
- `markdownlint --config .github/lint_config.json '**/*.md'`
-- 现场输出(本机复现):
-
-```text
-Cannot read or parse config file '.github/lint_config.json': ENOENT: no such file or directory, open '.github/lint_config.json'
-```
-
-### 2) 本地 `make lint` 与 CI 不一致(假通过)
-
-- `Makefile` 当前 lint 命令:`markdownlint **/*.md`
-- 在 `/bin/sh -> dash` 下,`**/*.md` 只会匹配“单层目录的 md”,不会递归覆盖 `assets/**`。
-- 结果:`make lint` 可能返回 0,但 CI 会真正 lint 全仓并失败。
-
-### 3) 关键“操作指引”仍引用旧路径
-
-- `assets/config/.codex/README.md` 仍要求复制 `config/.codex/...`(实际路径已迁到 `assets/config/.codex/...`)。
-- `assets/skills/skills-skills/references/*.md` 示例仍写 `./skills/...`(实际应为 `./assets/skills/...`)。
-
-### 4) 忽略规则偏差导致工作区污染
-
-- `.gitignore` 仍忽略 `backups/gz/`(旧位置),但当前备份落在 `assets/repo/backups/gz/`。
-- 现场信号:`git status` 出现 `?? assets/repo/backups/gz/`。
-
-## 约束矩阵(从仓库 AGENTS.md/资产规范提取)
-
-| 约束 | 来源 | 含义 |
-|---|---|---|
-| 不自动修改 `.github/workflows/*.yml` | 根 `AGENTS.md` | 优先“补配置/改命令”而不是改 CI 工作流 |
-| 不删除或覆盖 `assets/repo/backups/gz/` 存档 | 根 `AGENTS.md` | 不清理现有 `.tar.gz`,只能通过 ignore/流程避免污染 |
-| `assets/repo/` 第三方镜像少改动 | `assets/AGENTS.md` | 仅在影响入口/指引时做最小修改 |
-
-## 风险量化表
-
-| 风险点 | 严重程度 | 触发信号 (Signal) | 缓解方案 (Mitigation) |
-| :--- | :--- | :--- | :--- |
-| 通过“放宽 lint 配置”掩盖真实问题 | Medium | CI 绿但文档质量下降、后续难以收敛 | 配置要“最小放宽”,并在 PLAN 中记录哪些规则被禁用及原因 |
-| 为了 lint 大规模重排文档引入链接/引用破坏 | High | lychee/link-checker 或手工打开出现断链 | 优先改配置与关键入口文档;如果必须改文档,限定范围并每步做 link/rg 校验 |
-| 继续生成备份产物污染工作区 | Medium | `git status` 持续出现 `assets/repo/backups/gz/` | `.gitignore` 增加 `assets/repo/backups/gz/`,并在脚本说明中明确输出位置 |
-
-## 假设与证伪(执行 Agent 必跑)
-
-| 假设 | 默认假设 | 证伪命令 |
-|---|---|---|
-| CI 失败主因是缺 `.github/lint_config.json` | 是 | `ls -la .github/lint_config.json` |
-| 修复 `.github/lint_config.json` 后仍会有 lint 违规 | 是(已见多条) | `markdownlint --config .github/lint_config.json '**/*.md'` |
-| `make lint` 未覆盖 `assets/**` | 是 | `make -n lint` + 对比 `markdownlint '**/*.md'` 的输出范围 |
-
diff --git a/assets/tasks/0001-repair-ci-lint-and-paths/PLAN.md b/assets/tasks/0001-repair-ci-lint-and-paths/PLAN.md
deleted file mode 100644
index 7222173..0000000
--- a/assets/tasks/0001-repair-ci-lint-and-paths/PLAN.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# PLAN — 决策与路径
-
-## 目标
-
-在保持改动范围最小的前提下,让:
-
-1. CI 的 `markdown-lint` 阶段可执行且退出 0;
-2. 本地 `make lint` 与 CI 完全一致(不会假通过);
-3. 迁移后仍会误导执行的旧路径指引被修复;
-4. `.gitignore` 与新结构一致,避免产物污染工作区。
-
-## 方案对比(至少两种)
-
-### 方案 A(推荐):补齐 `.github/lint_config.json` + 对齐 Makefile + 最小修文档/忽略
-
-**做法**
-- 新增 `.github/lint_config.json`,以“最小放宽”让现有文档可通过。
-- 更新 `Makefile`:使用与 CI 相同的 glob 形式(引用 `'**/*.md'`),并指向同一 config。
-- 修复少量关键文档旧路径(仅指引类 README/参考文档)。
-- 更新 `.gitignore` 覆盖 `assets/repo/backups/gz/` 与迁移后的 venv 路径。
-
-**Pros**
-- 改动小、可回滚、最容易让 CI 回绿。
-- 不需要全仓重排 Markdown。
-
-**Cons**
-- 可能需要在 lint config 中禁用部分规则(需要记录原因,避免“质量坍塌”)。
-
-### 方案 B:严格 lint(少放宽)+ 大范围修正文档格式
-
-**做法**
-- 补齐 `.github/lint_config.json`,尽量保持严格规则;
-- 逐个修正文档以满足规则(可能涉及大量文件)。
-
-**Pros**
-- 长期文档质量更稳定,规则更强约束。
-
-**Cons**
-- 成本巨大、风险高(断链/误改内容/大 PR 难回滚),不符合“最少修改原则”。
-
-## 决策
-
-选择 **方案 A**。若后续需要提升文档质量,再开独立任务做“严格化 + 大规模修复”,避免一次 PR 混入两类目标。
-
-## 逻辑流图(Mermaid)
-
-```mermaid
-flowchart LR
- dev_make[make lint] --> lint_cmd[markdownlint]
- ci[GitHub Actions CI] --> lint_cmd
- lint_cmd --> config[.github/lint_config.json]
- lint_cmd --> glob['**/*.md']
-```
-
-## 原子变更清单(文件级,不写代码)
-
-1. 新增:`.github/lint_config.json`(最小放宽,明确记录禁用项原因)。
-2. 修改:`Makefile` 的 `lint` 目标:
- - 使用 quoted glob:`'**/*.md'`
- - 使用 `--config .github/lint_config.json`
-3. 修改:`.gitignore`:
- - 忽略 `assets/repo/backups/gz/`
- - 更新旧路径忽略项(如 `skills/skills-skills/...` → `assets/skills/...`)
-4. 修改:`assets/config/.codex/README.md`(复制命令路径纠正)。
-5. 修改:`assets/skills/skills-skills/references/*.md`(示例路径纠正)。
-6. 评估(可选):是否需要限定 lint 范围/忽略 `assets/repo/**`(若第三方镜像导致无法通过)。
-
-## 回滚协议(自愈步骤)
-
-若任何一步导致 lint 或文档入口不可用:
-
-1. `git status --porcelain=v1` 确认改动文件集合
-2. `git restore --staged --worktree ` 回退到改动前(或 `git revert ` 若已提交)
-3. 重新跑:
- - `make lint`
- - `markdownlint --config .github/lint_config.json '**/*.md'`
-4. 若仅 `.github/lint_config.json` 导致行为异常,可先临时移除该文件再复验,以确认根因
-
diff --git a/assets/tasks/0001-repair-ci-lint-and-paths/README.md b/assets/tasks/0001-repair-ci-lint-and-paths/README.md
deleted file mode 100644
index bedcbc0..0000000
--- a/assets/tasks/0001-repair-ci-lint-and-paths/README.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# 0001 — repair-ci-lint-and-paths
-
-本任务把“路径迁移后审计发现的阻塞项”收敛为一组可执行修复:让 CI 可跑通、让本地 `make lint` 与 CI 一致、并清理会误导执行的旧路径指引。
-
-## In Scope
-
-- 修复 CI `markdown-lint` 阶段的硬错误(缺失 `.github/lint_config.json`)。
-- 修复本地 `make lint` 的“假通过”(使其覆盖 `assets/**` 深层 Markdown,并与 CI 使用同一套 glob/配置)。
-- 修复迁移后仍会误导执行的旧路径指引(以“仓库自有文档”为主,第三方镜像按最小原则处理):
- - `assets/config/.codex/README.md` 的复制路径。
- - `assets/skills/skills-skills/references/*.md` 的示例路径(`skills/...` → `assets/skills/...`)。
-- 修复忽略规则偏差(`.gitignore`)以匹配新结构:
- - 忽略 `assets/repo/backups/gz/`(避免审计/备份产物污染工作区)。
- - 更新旧路径忽略项 `skills/skills-skills/...` → `assets/skills/...`(如仍需要)。
-
-## Out of Scope
-
-- 不做“全仓 Markdown 大规模重排/格式化”(除非为让 lint 过关的最小改动)。
-- 不对 `assets/repo/` 下第三方镜像/子模块做“顺手修文档/批量替换”,除非它直接影响本仓库的入口可运行性与操作指引。
-- 不修改 `.github/workflows/*.yml`(优先通过补齐/修复配置文件与命令一致性解决)。
-
-## 执行顺序(必须按此阅读/执行)
-
-1. `CONTEXT.md`:现状证据、约束、风险与假设
-2. `ACCEPTANCE.md`:验收标准(执行前先确认目标)
-3. `PLAN.md`:方案对比、决策与回滚协议
-4. `TODO.md`:按 P0→P2 执行,每步必须跑 Verify
-5. `STATUS.md`:执行中持续记录证据与状态
-
diff --git a/assets/tasks/0001-repair-ci-lint-and-paths/STATUS.md b/assets/tasks/0001-repair-ci-lint-and-paths/STATUS.md
deleted file mode 100644
index 149821c..0000000
--- a/assets/tasks/0001-repair-ci-lint-and-paths/STATUS.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# STATUS — 任务真相源
-
-## 状态机
-
-- Status: **Done**
-- Owner: 执行 Agent(待分配)
-- Last Updated: 2026-02-27
-
-## 仓库状态快照(Live Evidence)
-
-### 分支与工作区
-
-```text
-branch: develop
-dirty: ?? assets/repo/backups/gz/
-```
-
-### 关键失败信号(CI 等价命令)
-
-```text
-$ markdownlint --config .github/lint_config.json '**/*.md'
-Cannot read or parse config file '.github/lint_config.json': ENOENT: no such file or directory, open '.github/lint_config.json'
-```
-
-### 候选配置验证(在 tasks 内部验证通过)
-
-```text
-$ markdownlint --config assets/tasks/0001-repair-ci-lint-and-paths/artifacts/lint_config.candidate.json '**/*.md'
-EXIT:0
-sha256: cac90c3741a847e181cc184cc593778467fad70da85acb683545608612a77cda
-```
-
-### 可应用补丁准备完成(仅生成补丁,不自动落地)
-
-```text
-$ git apply --check assets/tasks/0001-repair-ci-lint-and-paths/patches/0001-fix-ci-lint-and-paths.patch
-OK
-```
-
-### 补丁已落地(仓库根验证)
-
-```text
-$ markdownlint --config .github/lint_config.json '**/*.md'
-EXIT:0
-
-$ make lint
-EXIT:0
-
-$ git status --porcelain=v1
-(无 assets/repo/backups/gz 噪音)
-```
-
-### `make lint` 当前行为(存在假通过风险)
-
-```text
-Makefile: markdownlint **/*.md
-/bin/sh -> dash(不支持 globstar)
-```
-
-## 阻塞详情
-
-- Blocked by: 无
-
-## 已执行命令(审计阶段)
-
-- `git status --porcelain=v1`
-- `sed -n '1,200p' Makefile`
-- `sed -n '1,220p' .github/workflows/ci.yml`
-- `markdownlint --config .github/lint_config.json '**/*.md'`
-- `make lint`
-- `git apply assets/tasks/0001-repair-ci-lint-and-paths/patches/0001-fix-ci-lint-and-paths.patch`
-- `markdownlint --config .github/lint_config.json '**/*.md'`
diff --git a/assets/tasks/0001-repair-ci-lint-and-paths/TODO.md b/assets/tasks/0001-repair-ci-lint-and-paths/TODO.md
deleted file mode 100644
index d784a15..0000000
--- a/assets/tasks/0001-repair-ci-lint-and-paths/TODO.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# TODO — 可执行清单(按 P0→P2)
-
-> 格式:`[ ] Px: <动作> | Verify: <验证手段> | Gate: <准入>`
-
-## P0(必须先做,阻塞 CI/开发体验)
-
-- [x] P0: 应用补丁(落地修复) | Verify: `git apply --check assets/tasks/0001-repair-ci-lint-and-paths/patches/0001-fix-ci-lint-and-paths.patch` | Gate: 输出 `OK`
-- [x] P0: 应用补丁(执行) | Verify: `git apply assets/tasks/0001-repair-ci-lint-and-paths/patches/0001-fix-ci-lint-and-paths.patch` | Gate: ExitCode=0
-- [x] P0: 创建 `.github/lint_config.json`(最小放宽,保证可落地) | Verify: `cat .github/lint_config.json` | Gate: 文件存在且 JSON 可解析
-- [x] P0: 让 `markdownlint --config .github/lint_config.json '**/*.md'` 退出 0 | Verify: `markdownlint --config .github/lint_config.json '**/*.md'` | Gate: ExitCode=0
-- [x] P0: 对齐本地 `make lint` 与 CI(同 config + 同 glob) | Verify: `make lint` | Gate: ExitCode=0 且覆盖 `assets/**`
-- [x] P0: 修复 `assets/config/.codex/README.md` 复制路径 | Verify: `rg -n "cp -f config/\\.codex" assets/config/.codex/README.md || true` | Gate: 无匹配
-- [x] P0: 修复 `assets/skills/skills-skills/references/*.md` 旧示例路径 | Verify: `rg -n "\\./skills/skills-skills" assets/skills/skills-skills/references -S || true` | Gate: 无匹配
-
-## P1(清理迁移遗留,减少未来误操作)
-
-- [ ] P1: 更新 `.gitignore` 忽略 `assets/repo/backups/gz/` | Verify: `rg -n "^assets/repo/backups/gz/" .gitignore` | Gate: 有匹配
-- [ ] P1: 更新 `.gitignore` 中旧路径 `skills/skills-skills/...` | Verify: `rg -n "skills/skills-skills/scripts/\\.venv-skill-seekers" .gitignore || true` | Gate: 旧条目移除或替换为 `assets/skills/...`
-- [ ] P1: 决定是否将 lint 范围排除 `assets/repo/**`(第三方镜像) | Verify: `markdownlint --config .github/lint_config.json '**/*.md'` | Gate: 无需改动第三方大量文件也能通过
-
-## P2(可选增强)
-
-- [ ] P2: 为 lint/CI 策略写入“为什么这样配置”的注释/说明文档 | Verify: `rg -n "lint_config" -S README.md AGENTS.md .github/workflows/ci.yml` | Gate: 维护者能读懂规则与边界
-- [ ] P2: 增加一个最小 smoke 校验脚本(可选) | Verify: `bash -lc '
+```
+
+**Capture once and export multiple formats**
+```javascript
+const result = await snapdom(document.querySelector("#target"));
+const png = await result.toPng();
+const svg = await result.toSvg();
+const canvas = await result.toCanvas();
+```
+
+**One-step PNG export**
+```javascript
+const png = await snapdom.toPng(document.querySelector("#target"));
+```
+
+**Download an element**
+```javascript
+await snapdom.download(document.querySelector("#target"), "screenshot.png");
+```
+
+**Set scale and dimensions**
+```javascript
+const png = await snapdom.toPng(element, {
+ scale: 2,
+ width: 1200,
+ height: 630,
+});
+```
+
+**Exclude UI controls**
+```javascript
+const png = await snapdom.toPng(element, {
+ exclude: ".controls, [data-no-capture]",
+});
+```
+
+**Use a CORS proxy fallback**
+```javascript
+const png = await snapdom.toPng(element, {
+ useProxy: "https://cors.example.com/?",
+});
+```
+
+## Examples
+
+### Example 1: Social Card Export
+
+- Input: element `#card`, target size `1200x630`, PNG output.
+- Steps:
+ 1. Ensure fonts and images are loaded.
+ 2. Capture with explicit `width`, `height`, and `scale`.
+ 3. Download or upload the resulting PNG.
+- Expected output / acceptance: exported image matches the card bounds and excludes editor controls.
+
+### Example 2: SVG Snapshot for Documentation
+
+- Input: styled component preview.
+- Steps:
+ 1. Call `snapdom(element)`.
+ 2. Export `toSvg()` for scalable documentation output.
+ 3. Inspect missing fonts/assets if the snapshot differs from the page.
+- Expected output / acceptance: SVG preserves visible styles and remains inspectable as a vector artifact.
+
+### Example 3: CORS Asset Triage
+
+- Input: export shows missing remote images.
+- Steps:
+ 1. Confirm whether assets are same-origin and CORS-enabled.
+ 2. Retry with `useProxy` or replace remote assets with local/same-origin URLs.
+ 3. Validate final output in the target browser.
+- Expected output / acceptance: missing images are attributed to CORS, loading, or selector/sizing issues.
+
+## References
+
+- `references/index.md`: local snapDOM reference navigation.
+- `references/other.md`: generated upstream notes and API details.
+
+## Maintenance
+
+- Sources: local `references/` extracted from snapDOM documentation.
+- Last updated: 2026-04-28
+- Known limits: screenshot fidelity depends on browser support, loaded assets, CORS, fonts, and installed snapDOM version.
diff --git a/assets/skills/polymarket/assets/.gitkeep b/skills/snapdom/assets/.gitkeep
similarity index 100%
rename from assets/skills/polymarket/assets/.gitkeep
rename to skills/snapdom/assets/.gitkeep
diff --git a/assets/skills/snapdom/references/index.md b/skills/snapdom/references/index.md
similarity index 100%
rename from assets/skills/snapdom/references/index.md
rename to skills/snapdom/references/index.md
diff --git a/assets/skills/snapdom/references/other.md b/skills/snapdom/references/other.md
similarity index 100%
rename from assets/skills/snapdom/references/other.md
rename to skills/snapdom/references/other.md
diff --git a/assets/skills/polymarket/scripts/.gitkeep b/skills/snapdom/scripts/.gitkeep
similarity index 100%
rename from assets/skills/polymarket/scripts/.gitkeep
rename to skills/snapdom/scripts/.gitkeep
diff --git a/assets/skills/sop-generator/SKILL.md b/skills/sop-generator/SKILL.md
similarity index 96%
rename from assets/skills/sop-generator/SKILL.md
rename to skills/sop-generator/SKILL.md
index de4c225..1efaf7f 100644
--- a/assets/skills/sop-generator/SKILL.md
+++ b/skills/sop-generator/SKILL.md
@@ -81,12 +81,12 @@ SOP 标题 / 文档编号
### Pattern 5: 一键生成 SOP 模板(脚本)
```text
-python3 assets/skills/sop-generator/scripts/generate_sop.py --title "项目上线 SOP" --doc-id "SOP-001" --version "v1.0" --output sop.md
+python3 skills/sop-generator/scripts/generate_sop.py --title "项目上线 SOP" --doc-id "SOP-001" --version "v1.0" --output sop.md
```
### Pattern 6: 生成最小可行 SOP(MVP)
```text
-python3 assets/skills/sop-generator/scripts/generate_sop.py --title "巡检 SOP" --mvp --output sop-mvp.md
+python3 skills/sop-generator/scripts/generate_sop.py --title "巡检 SOP" --mvp --output sop-mvp.md
```
## 规则与约束
diff --git a/assets/skills/sop-generator/assets/record-log-template.md b/skills/sop-generator/assets/record-log-template.md
similarity index 100%
rename from assets/skills/sop-generator/assets/record-log-template.md
rename to skills/sop-generator/assets/record-log-template.md
diff --git a/assets/skills/sop-generator/assets/sop-mvp.md b/skills/sop-generator/assets/sop-mvp.md
similarity index 100%
rename from assets/skills/sop-generator/assets/sop-mvp.md
rename to skills/sop-generator/assets/sop-mvp.md
diff --git a/assets/skills/sop-generator/assets/sop-template.md b/skills/sop-generator/assets/sop-template.md
similarity index 100%
rename from assets/skills/sop-generator/assets/sop-template.md
rename to skills/sop-generator/assets/sop-template.md
diff --git a/assets/skills/sop-generator/references/examples.md b/skills/sop-generator/references/examples.md
similarity index 100%
rename from assets/skills/sop-generator/references/examples.md
rename to skills/sop-generator/references/examples.md
diff --git a/assets/skills/sop-generator/references/index.md b/skills/sop-generator/references/index.md
similarity index 100%
rename from assets/skills/sop-generator/references/index.md
rename to skills/sop-generator/references/index.md
diff --git a/assets/skills/sop-generator/references/repo-vibe-kanban.md b/skills/sop-generator/references/repo-vibe-kanban.md
similarity index 100%
rename from assets/skills/sop-generator/references/repo-vibe-kanban.md
rename to skills/sop-generator/references/repo-vibe-kanban.md
diff --git a/assets/skills/sop-generator/references/sop-foundations.md b/skills/sop-generator/references/sop-foundations.md
similarity index 100%
rename from assets/skills/sop-generator/references/sop-foundations.md
rename to skills/sop-generator/references/sop-foundations.md
diff --git a/assets/skills/sop-generator/references/sop-structure.md b/skills/sop-generator/references/sop-structure.md
similarity index 100%
rename from assets/skills/sop-generator/references/sop-structure.md
rename to skills/sop-generator/references/sop-structure.md
diff --git a/assets/skills/sop-generator/references/writing-style.md b/skills/sop-generator/references/writing-style.md
similarity index 100%
rename from assets/skills/sop-generator/references/writing-style.md
rename to skills/sop-generator/references/writing-style.md
diff --git a/assets/skills/sop-generator/scripts/generate_sop.py b/skills/sop-generator/scripts/generate_sop.py
similarity index 100%
rename from assets/skills/sop-generator/scripts/generate_sop.py
rename to skills/sop-generator/scripts/generate_sop.py
diff --git a/skills/telegram-dev/SKILL.md b/skills/telegram-dev/SKILL.md
new file mode 100644
index 0000000..26d51c4
--- /dev/null
+++ b/skills/telegram-dev/SKILL.md
@@ -0,0 +1,139 @@
+---
+name: telegram-dev
+description: "Telegram development skill: Bot API, Mini Apps/Web Apps, webhooks, long polling, inline/reply keyboards, payments, initData validation, TDLib/MTProto, message formatting, and deployment troubleshooting."
+---
+
+# telegram-dev Skill
+
+Use this skill to build Telegram bots, Mini Apps, and client integrations with explicit security boundaries around tokens, webhooks, and user data.
+
+## When to Use This Skill
+
+Trigger when any of these applies:
+- Creating or debugging a Telegram Bot with Bot API methods, long polling, webhooks, commands, messages, media, files, or payments.
+- Building Telegram Mini Apps/Web Apps with `window.Telegram.WebApp`, buttons, theme params, storage, sensors, or `initData` validation.
+- Implementing inline keyboards, reply keyboards, callback queries, command menus, or dynamic aligned message views.
+- Working with TDLib/MTProto client development or API ID/hash based integrations.
+- Troubleshooting webhook TLS/port issues, bot token errors, callback handling, formatting, or deployment.
+
+## Not For / Boundaries
+
+- Not for spam, unauthorized scraping, account abuse, or bypassing Telegram platform rules.
+- Never commit or print bot tokens, API hash, API ID plus phone session data, payment secrets, or user private data.
+- Webhook examples require HTTPS and public reachability; local-only servers need a tunnel or local Bot API server setup.
+- Required inputs: Bot vs Mini App vs TDLib scope, language/framework, token/auth status, update payload, deployment URL, and exact error.
+- Telegram APIs evolve; verify current method parameters and limits in official docs when precision matters.
+
+## Quick Reference
+
+### Common Patterns
+
+**Bot API endpoint shape**
+```text
+https://api.telegram.org/bot/
+```
+
+**Send a message**
+```python
+import requests
+
+requests.post(
+ f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
+ json={"chat_id": chat_id, "text": "Hello"},
+ timeout=10,
+)
+```
+
+**Long polling**
+```python
+updates = requests.get(
+ f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates",
+ params={"offset": offset, "timeout": 30},
+ timeout=35,
+).json()
+```
+
+**Set a webhook**
+```python
+requests.post(
+ f"https://api.telegram.org/bot{BOT_TOKEN}/setWebhook",
+ json={"url": "https://example.com/webhook"},
+ timeout=10,
+)
+```
+
+**Inline keyboard**
+```python
+reply_markup = {
+ "inline_keyboard": [[
+ {"text": "Open", "url": "https://example.com"},
+ {"text": "Action", "callback_data": "action:1"},
+ ]]
+}
+```
+
+**Answer a callback query**
+```python
+requests.post(
+ f"https://api.telegram.org/bot{BOT_TOKEN}/answerCallbackQuery",
+ json={"callback_query_id": callback_query_id, "text": "OK"},
+)
+```
+
+**Initialize a Mini App**
+```javascript
+const tg = window.Telegram.WebApp;
+tg.ready();
+tg.expand();
+```
+
+**Send Mini App data back to the bot**
+```javascript
+tg.sendData(JSON.stringify({ action: "submit" }));
+```
+
+**Validate Mini App initData server-side**
+```text
+Parse initData -> remove hash -> sort key=value pairs -> HMAC with WebAppData-derived secret -> compare hash.
+```
+
+## Examples
+
+### Example 1: Echo Bot with Long Polling
+
+- Input: bot token and a private test chat.
+- Steps:
+ 1. Call `getUpdates` with an offset.
+ 2. Extract `message.chat.id` and `message.text`.
+ 3. Reply with `sendMessage` and advance offset.
+- Expected output / acceptance: each user message gets one reply and old updates are not processed repeatedly.
+
+### Example 2: Webhook Deployment
+
+- Input: HTTPS URL `https://example.com/webhook`.
+- Steps:
+ 1. Deploy an endpoint that accepts POST JSON updates.
+ 2. Call `setWebhook` with the public URL.
+ 3. Use `getWebhookInfo` to verify status and last error.
+- Expected output / acceptance: Telegram delivers updates to the endpoint and webhook info has no current delivery error.
+
+### Example 3: Mini App Button Flow
+
+- Input: web app URL and bot chat.
+- Steps:
+ 1. Send a reply or inline keyboard button with `web_app.url`.
+ 2. In the Mini App, call `ready()` and validate `initData` on the backend.
+ 3. Send final data with `sendData` or a backend API call.
+- Expected output / acceptance: Mini App opens inside Telegram, backend authenticates the user, and bot receives structured data.
+
+## References
+
+- `references/index.md`: Telegram ecosystem navigation and official links.
+- `references/Telegram_Bot_按钮和键盘实现模板.md`: button and keyboard implementation templates.
+- `references/动态视图对齐实现文档.md`: aligned data display and dynamic message formatting.
+
+## Maintenance
+
+- Sources: local Telegram reference files plus official links listed in `references/index.md`.
+- Last updated: 2026-04-28
+- Known limits: API methods, limits, and Mini App capabilities are version-sensitive; verify against official Telegram docs for production releases.
diff --git a/assets/skills/postgresql/assets/.gitkeep b/skills/telegram-dev/assets/.gitkeep
similarity index 100%
rename from assets/skills/postgresql/assets/.gitkeep
rename to skills/telegram-dev/assets/.gitkeep
diff --git a/assets/skills/telegram-dev/references/Telegram_Bot_按钮和键盘实现模板.md b/skills/telegram-dev/references/Telegram_Bot_按钮和键盘实现模板.md
similarity index 100%
rename from assets/skills/telegram-dev/references/Telegram_Bot_按钮和键盘实现模板.md
rename to skills/telegram-dev/references/Telegram_Bot_按钮和键盘实现模板.md
diff --git a/assets/skills/telegram-dev/references/index.md b/skills/telegram-dev/references/index.md
similarity index 100%
rename from assets/skills/telegram-dev/references/index.md
rename to skills/telegram-dev/references/index.md
diff --git a/assets/skills/telegram-dev/references/动态视图对齐实现文档.md b/skills/telegram-dev/references/动态视图对齐实现文档.md
similarity index 100%
rename from assets/skills/telegram-dev/references/动态视图对齐实现文档.md
rename to skills/telegram-dev/references/动态视图对齐实现文档.md
diff --git a/assets/skills/postgresql/scripts/.gitkeep b/skills/telegram-dev/scripts/.gitkeep
similarity index 100%
rename from assets/skills/postgresql/scripts/.gitkeep
rename to skills/telegram-dev/scripts/.gitkeep
diff --git a/skills/timescaledb/SKILL.md b/skills/timescaledb/SKILL.md
new file mode 100644
index 0000000..832684e
--- /dev/null
+++ b/skills/timescaledb/SKILL.md
@@ -0,0 +1,128 @@
+---
+name: timescaledb
+description: "TimescaleDB time-series skill: PostgreSQL extension setup, hypertables, time_bucket queries, continuous aggregates, compression/columnstore, retention, performance, and migration troubleshooting."
+---
+
+# timescaledb Skill
+
+Use this skill to model and operate time-series workloads on TimescaleDB/Tiger Data using hypertables, time buckets, compression, and continuous aggregates.
+
+## When to Use This Skill
+
+Trigger when any of these applies:
+- Creating or migrating PostgreSQL tables into TimescaleDB hypertables.
+- Designing time-series schemas, chunk intervals, indexes, retention, or compression/columnstore policies.
+- Writing `time_bucket` analytics queries or continuous aggregates.
+- Debugging ingestion performance, query performance, refresh policies, or migration issues.
+- Comparing plain PostgreSQL tables with TimescaleDB hypertables for event/time-series data.
+
+## Not For / Boundaries
+
+- Not a replacement for PostgreSQL fundamentals; use `postgresql` for generic SQL, transactions, roles, and non-time-series schema work.
+- Do not enable retention/compression policies on production data without restore-tested backups and data-loss review.
+- Continuous aggregates have version-specific behavior; verify real-time aggregation and refresh policy defaults against the installed version.
+- Required inputs: TimescaleDB version, PostgreSQL version, table schema, time column, ingest rate, query patterns, retention/compression goals, and error text.
+- Tiger Cloud features and self-hosted extension features may differ; verify deployment type before prescribing commands.
+
+## Quick Reference
+
+### Common Patterns
+
+**Enable the extension**
+```sql
+CREATE EXTENSION IF NOT EXISTS timescaledb;
+```
+
+**Create a time-series table**
+```sql
+CREATE TABLE conditions (
+ time timestamptz NOT NULL,
+ location text NOT NULL,
+ temperature double precision,
+ humidity double precision
+);
+```
+
+**Convert a table to a hypertable**
+```sql
+SELECT create_hypertable('conditions', 'time');
+```
+
+**Bucket raw data by hour**
+```sql
+SELECT
+ time_bucket('1 hour', time) AS bucket,
+ location,
+ avg(temperature) AS avg_temp
+FROM conditions
+GROUP BY bucket, location
+ORDER BY bucket DESC;
+```
+
+**Create a continuous aggregate**
+```sql
+CREATE MATERIALIZED VIEW conditions_hourly
+WITH (timescaledb.continuous) AS
+SELECT
+ time_bucket('1 hour', time) AS bucket,
+ location,
+ avg(temperature) AS avg_temp
+FROM conditions
+GROUP BY bucket, location;
+```
+
+**Inspect hypertable size**
+```sql
+SELECT * FROM hypertable_detailed_size('conditions');
+```
+
+**Verify extension version**
+```sql
+SELECT extversion FROM pg_extension WHERE extname = 'timescaledb';
+```
+
+## Examples
+
+### Example 1: Convert Metrics Table to Hypertable
+
+- Input: existing table `conditions(time, location, temperature, humidity)`.
+- Steps:
+ 1. Confirm `time` is `NOT NULL` and uses a timestamp type.
+ 2. Enable the extension.
+ 3. Run `create_hypertable` in staging and test inserts/queries.
+- Expected output / acceptance: the table is a hypertable and existing time-range queries still return correct rows.
+
+### Example 2: Add Hourly Rollups
+
+- Input: dashboard needs hourly average temperature by location.
+- Steps:
+ 1. Write the raw `time_bucket('1 hour', time)` query.
+ 2. Convert it to a continuous aggregate after correctness is verified.
+ 3. Add a refresh policy only after deciding freshness and backfill windows.
+- Expected output / acceptance: dashboard reads from the aggregate with documented refresh expectations.
+
+### Example 3: Performance Triage
+
+- Input: slow time-range query over a hypertable.
+- Steps:
+ 1. Run `EXPLAIN (ANALYZE, BUFFERS)` and confirm chunk pruning.
+ 2. Check time predicate shape and indexes for dimension filters.
+ 3. Inspect hypertable/chunk size and compression state before changing policies.
+- Expected output / acceptance: root cause is classified as missing time predicate, bad index, chunk sizing, compression side effect, or stale stats.
+
+## References
+
+- `references/index.md`: navigation for local TimescaleDB references.
+- `references/installation.md`: install and deployment notes.
+- `references/hypertables.md`: hypertables, chunks, sizing, and related APIs.
+- `references/time_buckets.md`: `time_bucket` usage.
+- `references/continuous_aggregates.md`: aggregate creation and refresh behavior.
+- `references/compression.md`: compression/columnstore guidance.
+- `references/performance.md`: performance notes.
+- `references/tutorials.md`: walkthroughs and examples.
+
+## Maintenance
+
+- Sources: local `references/` extracted from TimescaleDB/Tiger Data documentation.
+- Last updated: 2026-04-28
+- Known limits: examples use common SQL forms; verify exact function signatures and policy defaults against the installed extension version.
diff --git a/assets/skills/proxychains/assets/.gitkeep b/skills/timescaledb/assets/.gitkeep
similarity index 100%
rename from assets/skills/proxychains/assets/.gitkeep
rename to skills/timescaledb/assets/.gitkeep
diff --git a/skills/timescaledb/references/CONTRIBUTING.md b/skills/timescaledb/references/CONTRIBUTING.md
new file mode 100644
index 0000000..03bfd0a
--- /dev/null
+++ b/skills/timescaledb/references/CONTRIBUTING.md
@@ -0,0 +1,8 @@
+# TimescaleDB Docs Contributing(外链)
+
+本仓库未在 `skills/timescaledb/references/` 内维护完整贡献指南原文。
+
+请参考官方贡献指南(Docs 仓库):
+
+- https://github.com/timescale/docs/blob/latest/CONTRIBUTING.md
+
diff --git a/assets/skills/timescaledb/references/api.md b/skills/timescaledb/references/api.md
similarity index 100%
rename from assets/skills/timescaledb/references/api.md
rename to skills/timescaledb/references/api.md
diff --git a/assets/skills/timescaledb/references/compression.md b/skills/timescaledb/references/compression.md
similarity index 100%
rename from assets/skills/timescaledb/references/compression.md
rename to skills/timescaledb/references/compression.md
diff --git a/assets/skills/timescaledb/references/continuous_aggregates.md b/skills/timescaledb/references/continuous_aggregates.md
similarity index 100%
rename from assets/skills/timescaledb/references/continuous_aggregates.md
rename to skills/timescaledb/references/continuous_aggregates.md
diff --git a/assets/skills/timescaledb/references/getting_started.md b/skills/timescaledb/references/getting_started.md
similarity index 100%
rename from assets/skills/timescaledb/references/getting_started.md
rename to skills/timescaledb/references/getting_started.md
diff --git a/assets/skills/timescaledb/references/hyperfunctions.md b/skills/timescaledb/references/hyperfunctions.md
similarity index 100%
rename from assets/skills/timescaledb/references/hyperfunctions.md
rename to skills/timescaledb/references/hyperfunctions.md
diff --git a/assets/skills/timescaledb/references/hypertables.md b/skills/timescaledb/references/hypertables.md
similarity index 100%
rename from assets/skills/timescaledb/references/hypertables.md
rename to skills/timescaledb/references/hypertables.md
diff --git a/assets/skills/timescaledb/references/index.md b/skills/timescaledb/references/index.md
similarity index 100%
rename from assets/skills/timescaledb/references/index.md
rename to skills/timescaledb/references/index.md
diff --git a/assets/skills/timescaledb/references/installation.md b/skills/timescaledb/references/installation.md
similarity index 100%
rename from assets/skills/timescaledb/references/installation.md
rename to skills/timescaledb/references/installation.md
diff --git a/assets/skills/timescaledb/references/llms-full.md b/skills/timescaledb/references/llms-full.md
similarity index 100%
rename from assets/skills/timescaledb/references/llms-full.md
rename to skills/timescaledb/references/llms-full.md
diff --git a/assets/skills/timescaledb/references/llms.md b/skills/timescaledb/references/llms.md
similarity index 100%
rename from assets/skills/timescaledb/references/llms.md
rename to skills/timescaledb/references/llms.md
diff --git a/assets/skills/timescaledb/references/other.md b/skills/timescaledb/references/other.md
similarity index 100%
rename from assets/skills/timescaledb/references/other.md
rename to skills/timescaledb/references/other.md
diff --git a/assets/skills/timescaledb/references/performance.md b/skills/timescaledb/references/performance.md
similarity index 100%
rename from assets/skills/timescaledb/references/performance.md
rename to skills/timescaledb/references/performance.md
diff --git a/assets/skills/timescaledb/references/time_buckets.md b/skills/timescaledb/references/time_buckets.md
similarity index 100%
rename from assets/skills/timescaledb/references/time_buckets.md
rename to skills/timescaledb/references/time_buckets.md
diff --git a/assets/skills/timescaledb/references/tutorials.md b/skills/timescaledb/references/tutorials.md
similarity index 100%
rename from assets/skills/timescaledb/references/tutorials.md
rename to skills/timescaledb/references/tutorials.md
diff --git a/assets/skills/snapdom/assets/.gitkeep b/skills/timescaledb/scripts/.gitkeep
similarity index 100%
rename from assets/skills/snapdom/assets/.gitkeep
rename to skills/timescaledb/scripts/.gitkeep
diff --git a/skills/tmux-autopilot/AGENTS.md b/skills/tmux-autopilot/AGENTS.md
new file mode 100644
index 0000000..58e10c9
--- /dev/null
+++ b/skills/tmux-autopilot/AGENTS.md
@@ -0,0 +1,33 @@
+# skills/tmux-autopilot
+
+本目录是 tmux 自动化操控技能:面向 AI 终端巡检、按键注入、蜂群协作与 oh-my-tmux 配置复用。
+
+## 目录结构
+
+```text
+skills/tmux-autopilot/
+├── AGENTS.md # 本文件:目录职责、结构与边界
+├── SKILL.md # 技能入口:触发条件、命令片段、规则与质量门
+├── assets/ # 外部 tmux 相关仓库的只读软链接入口
+│ ├── oh-my-tmux -> ../../../tools/external/.tmux
+│ └── tmux-src -> ../../../tools/external/tmux
+└── references/ # 长文档、示例与故障排查
+ ├── index.md
+ ├── getting_started.md
+ ├── api.md
+ ├── examples.md
+ └── troubleshooting.md
+```
+
+## 职责边界
+
+- `assets/oh-my-tmux` 只暴露 gpakosz/oh-my-tmux submodule,不在技能目录内复制配置源码。
+- `assets/tmux-src` 只暴露 tmux/tmux submodule,供需要查看源码或上游文档时定位。
+- 技能文档可以引用软链接入口;更新上游内容必须通过 `tools/external/` 下的 submodule 指针完成。
+- 不在本目录直接修改 submodule 内容;如需改造,先 fork 上游并更新 submodule 来源。
+
+## 依赖关系
+
+- 上游来源:`tools/external/.tmux` 与 `tools/external/tmux`。
+- 下游使用:`SKILL.md` 和 `references/` 中的命令示例。
+- 验证入口:`skills/auto-skill/scripts/validate-skill.sh skills/tmux-autopilot --strict`。
diff --git a/assets/skills/tmux-autopilot/SKILL.md b/skills/tmux-autopilot/SKILL.md
similarity index 87%
rename from assets/skills/tmux-autopilot/SKILL.md
rename to skills/tmux-autopilot/SKILL.md
index e940ae3..325bf36 100644
--- a/assets/skills/tmux-autopilot/SKILL.md
+++ b/skills/tmux-autopilot/SKILL.md
@@ -5,7 +5,7 @@ description: "tmux 自动化操控:读取/广播/救援 session|window|pane,
# tmux-autopilot Skill
-让 AI 像熟练运维一样操作 tmux:读取终端输出、发送按键、批量巡检、协作/救援其他终端,默认兼容 `assets/repo/.tmux`(gpakosz/oh-my-tmux)。
+让 AI 像熟练运维一样操作 tmux:读取终端输出、发送按键、批量巡检、协作/救援其他终端,默认兼容 `tools/external/.tmux`(gpakosz/oh-my-tmux),并在本技能内通过 `assets/oh-my-tmux` 与 `assets/tmux-src` 暴露上游 submodule。
## When to Use This Skill
@@ -14,7 +14,8 @@ description: "tmux 自动化操控:读取/广播/救援 session|window|pane,
- 需要向指定 pane 发送按键/命令(确认 `y`、`Enter`、`Ctrl+C`、广播同一窗口)。
- 需要批量巡检/接管多 AI 终端(蜂群协作、自动救援卡死任务)。
- 需要快速回忆 oh-my-tmux 快捷键、前缀或同步面板操作。
-- 需要在当前仓库复用 `assets/repo/.tmux` 配置并避免修改主配置。
+- 需要在当前仓库复用 `tools/external/.tmux` 配置并避免修改主配置。
+- 需要从技能目录内快速查看 oh-my-tmux 配置或 tmux 上游源码入口。
## Not For / Boundaries
@@ -85,8 +86,9 @@ tmux attach -t ai-hub
**启用 oh-my-tmux 配置(仓库内版本)**
```bash
-ln -sfn /home/lenovo/zip/vibe-coding-cn/assets/repo/.tmux/.tmux.conf ~/.tmux.conf
-cp -n /home/lenovo/zip/vibe-coding-cn/assets/repo/.tmux/.tmux.conf.local ~/.tmux.conf.local
+repo_root="$(git rev-parse --show-toplevel)"
+ln -sfn "$repo_root/skills/tmux-autopilot/assets/oh-my-tmux/.tmux.conf" ~/.tmux.conf
+cp -n "$repo_root/skills/tmux-autopilot/assets/oh-my-tmux/.tmux.conf.local" ~/.tmux.conf.local
```
**记录 pane 输出到文件**
@@ -153,20 +155,22 @@ tmux pipe-pane -t :. -o 'cat >> /tmp/tmux-- 目的:在需要更全面文档时,用仓库自带的 `Skill_Seekers-development` 自动抓取 gpakosz/.tmux 与 README,生成扩展参考文件,再手动筛选进 `references/`。
```bash
-cd /home/lenovo/zip/vibe-coding-cn/assets/repo/Skill_Seekers-development
+repo_root="$(git rev-parse --show-toplevel)"
+cd "$repo_root/tools/external/Skill_Seekers-development"
# 准备 Python 环境(如未安装)
uv tool install skill-seekers # 或 pip install skill-seekers
diff --git a/assets/skills/tmux-autopilot/references/getting_started.md b/skills/tmux-autopilot/references/getting_started.md
similarity index 86%
rename from assets/skills/tmux-autopilot/references/getting_started.md
rename to skills/tmux-autopilot/references/getting_started.md
index f63a15e..d1e6b86 100644
--- a/assets/skills/tmux-autopilot/references/getting_started.md
+++ b/skills/tmux-autopilot/references/getting_started.md
@@ -20,8 +20,9 @@
tmux -V
# 2) 软链配置(不会覆盖已有 .tmux.conf.local,如需自定义请编辑该文件)
-ln -sfn /home/lenovo/zip/vibe-coding-cn/assets/repo/.tmux/.tmux.conf ~/.tmux.conf
-cp -n /home/lenovo/zip/vibe-coding-cn/assets/repo/.tmux/.tmux.conf.local ~/.tmux.conf.local
+repo_root="$(git rev-parse --show-toplevel)"
+ln -sfn "$repo_root/skills/tmux-autopilot/assets/oh-my-tmux/.tmux.conf" ~/.tmux.conf
+cp -n "$repo_root/skills/tmux-autopilot/assets/oh-my-tmux/.tmux.conf.local" ~/.tmux.conf.local
# 3) 启动会话并验证前缀
tmux new -s demo -n shell
diff --git a/assets/skills/tmux-autopilot/references/index.md b/skills/tmux-autopilot/references/index.md
similarity index 54%
rename from assets/skills/tmux-autopilot/references/index.md
rename to skills/tmux-autopilot/references/index.md
index c52c682..907e22a 100644
--- a/assets/skills/tmux-autopilot/references/index.md
+++ b/skills/tmux-autopilot/references/index.md
@@ -10,5 +10,6 @@
## Notes
- 长文档、脚本细节放这里,`SKILL.md` 只保留可立即执行的片段。
-- 配置来源:仓库内 `assets/repo/.tmux`(gpakosz/oh-my-tmux)。
-- 大规模文档抓取/刷新可用 `assets/repo/Skill_Seekers-development`,示例见 `examples.md`。
+- 配置来源:技能内 `../assets/oh-my-tmux`,实际指向仓库 submodule `tools/external/.tmux`(gpakosz/oh-my-tmux)。
+- 源码来源:技能内 `../assets/tmux-src`,实际指向仓库 submodule `tools/external/tmux`。
+- 大规模文档抓取/刷新可用 `tools/external/Skill_Seekers-development`,示例见 `examples.md`。
diff --git a/assets/skills/tmux-autopilot/references/troubleshooting.md b/skills/tmux-autopilot/references/troubleshooting.md
similarity index 100%
rename from assets/skills/tmux-autopilot/references/troubleshooting.md
rename to skills/tmux-autopilot/references/troubleshooting.md
diff --git a/skills/twscrape/SKILL.md b/skills/twscrape/SKILL.md
new file mode 100644
index 0000000..2ba8202
--- /dev/null
+++ b/skills/twscrape/SKILL.md
@@ -0,0 +1,148 @@
+---
+name: twscrape
+description: "twscrape Twitter/X scraping skill: account pool setup, async search, user/tweet collection, CLI usage, proxy configuration, and rate-limit troubleshooting. Use when extracting public Twitter/X data with twscrape."
+---
+
+# twscrape Skill
+
+Use this skill to build or debug `twscrape` workflows for public Twitter/X data extraction with account rotation, async collection, CLI commands, and proxy-aware operation.
+
+## When to Use This Skill
+
+Trigger when any of these applies:
+- Scraping Twitter/X search results, profiles, followers, timelines, replies, retweeters, media, or trends with `twscrape`.
+- Setting up account pools, cookies, login flows, email verification, or account rotation.
+- Choosing between Python async API and the `twscrape` CLI.
+- Diagnosing rate limits, empty results, login failures, proxy failures, or suspended accounts.
+- Exporting normalized tweet/user data for monitoring, analytics, research, or archival pipelines.
+
+## Not For / Boundaries
+
+- Not for bypassing access controls, private content, paid-only data, or platform restrictions.
+- Not for guaranteed high-volume scraping; account health, platform changes, and endpoint limits can invalidate assumptions.
+- Do not place real Twitter/X passwords, cookies, email passwords, or proxy credentials in examples, commits, logs, or issue reports.
+- Required inputs: target query/user/tweet/list, collection limit, output format, account source, proxy requirements, and compliance constraints.
+- If behavior differs from these notes, verify against `references/` and the upstream repository before changing production collectors.
+
+## Quick Reference
+
+### Common Patterns
+
+**Install the library**
+```bash
+pip install twscrape
+```
+
+**Create an API client and add a cookie-backed account**
+```python
+from twscrape import API
+
+api = API("accounts.db")
+await api.pool.add_account(
+ "username",
+ "password",
+ "email@example.com",
+ "email-password",
+ cookies="ct0=...; auth_token=...",
+)
+```
+
+**Login all configured accounts**
+```python
+await api.pool.login_all()
+```
+
+**Search recent tweets**
+```python
+from twscrape import gather
+
+tweets = await gather(api.search("python lang:en", limit=50))
+```
+
+**Fetch a user then collect timeline data**
+```python
+user = await api.user_by_login("xdevelopers")
+tweets = await gather(api.user_tweets(user.id, limit=100))
+```
+
+**Collect followers or following**
+```python
+followers = await gather(api.followers(user.id, limit=100))
+following = await gather(api.following(user.id, limit=100))
+```
+
+**Inspect tweet details and replies**
+```python
+tweet = await api.tweet_details(1234567890)
+replies = await gather(api.tweet_replies(tweet.id, limit=50))
+```
+
+**Use the CLI for a small search**
+```bash
+twscrape search "python lang:en" --limit=20
+```
+
+**Manage accounts from the CLI**
+```bash
+twscrape add_accounts accounts.txt username:password:email:email_password
+twscrape login_accounts --manual
+twscrape accounts
+```
+
+**Set a global proxy**
+```bash
+export TWS_PROXY=socks5://user:pass@127.0.0.1:1080
+twscrape search "bitcoin" --limit=20
+```
+
+**Enable debug logging**
+```python
+from twscrape.logger import set_log_level
+
+set_log_level("DEBUG")
+```
+
+## Examples
+
+### Example 1: Search Export
+
+- Input: query `python lang:en`, limit `50`, output JSON Lines.
+- Steps:
+ 1. Confirm at least one healthy account with `twscrape accounts`.
+ 2. Use `await gather(api.search(query, limit=50))`.
+ 3. Serialize selected fields such as `id`, `date`, `user.username`, and `rawContent`.
+- Expected output / acceptance: a JSONL file with up to 50 tweet records and no credentials in logs.
+
+### Example 2: User Monitoring
+
+- Input: username `xdevelopers`, timeline limit `100`.
+- Steps:
+ 1. Resolve the account with `await api.user_by_login(username)`.
+ 2. Collect `api.user_tweets(user.id, limit=100)`.
+ 3. Store tweet IDs and timestamps so later runs can deduplicate.
+- Expected output / acceptance: user metadata plus a deduplicated timeline batch.
+
+### Example 3: Rate-Limit Triage
+
+- Input: collector returns no data or waits indefinitely.
+- Steps:
+ 1. Run `twscrape accounts` and identify locked, suspended, or rate-limited accounts.
+ 2. Enable debug logging and retry the smallest failing query.
+ 3. Add healthy accounts or wait for endpoint-specific reset before scaling up.
+- Expected output / acceptance: the failing mode is classified as account health, query syntax, proxy/network, or platform limit.
+
+## References
+
+- `references/index.md`: navigation for the local twscrape reference set.
+- `references/installation.md`: installation and dependency notes.
+- `references/account_management.md`: account pool, login, and rotation behavior.
+- `references/api_methods.md`: Python API method reference.
+- `references/cli_usage.md`: command-line usage.
+- `references/proxy_config.md`: proxy configuration and precedence.
+- `references/examples.md`: longer code examples and extraction patterns.
+
+## Maintenance
+
+- Sources: local `references/` extracted from upstream twscrape material and the upstream repository noted there.
+- Last updated: 2026-04-28
+- Known limits: Twitter/X endpoints and account policies change without notice; validate live collectors against a small sample before large runs.
diff --git a/assets/skills/snapdom/scripts/.gitkeep b/skills/twscrape/assets/.gitkeep
similarity index 100%
rename from assets/skills/snapdom/scripts/.gitkeep
rename to skills/twscrape/assets/.gitkeep
diff --git a/skills/twscrape/references/account_management.md b/skills/twscrape/references/account_management.md
new file mode 100644
index 0000000..acad33a
--- /dev/null
+++ b/skills/twscrape/references/account_management.md
@@ -0,0 +1,14 @@
+# twscrape:账号管理(参考)
+
+本文件为 `twscrape` 的账号管理参考页。
+
+## 推荐阅读顺序
+
+1. `skills/twscrape/references/installation.md`
+2. `skills/twscrape/SKILL.md`(包含账号池、登录、轮换等完整示例)
+3. `skills/twscrape/references/examples.md`
+
+## 入口
+
+- 账号添加/登录的核心示例:`skills/twscrape/SKILL.md`
+
diff --git a/skills/twscrape/references/api_methods.md b/skills/twscrape/references/api_methods.md
new file mode 100644
index 0000000..aa7e4e8
--- /dev/null
+++ b/skills/twscrape/references/api_methods.md
@@ -0,0 +1,8 @@
+# twscrape:API 方法(参考)
+
+本文件为 `twscrape` 的 API 方法参考页(仓库内精简版)。
+
+## 入口
+
+- 完整方法速查与示例:`skills/twscrape/SKILL.md`
+
diff --git a/skills/twscrape/references/cli_usage.md b/skills/twscrape/references/cli_usage.md
new file mode 100644
index 0000000..9aeb501
--- /dev/null
+++ b/skills/twscrape/references/cli_usage.md
@@ -0,0 +1,8 @@
+# twscrape:CLI 用法(参考)
+
+本文件为 `twscrape` 的命令行使用参考页。
+
+## 入口
+
+- CLI 相关命令示例:`skills/twscrape/SKILL.md`
+
diff --git a/assets/skills/twscrape/references/examples.md b/skills/twscrape/references/examples.md
similarity index 100%
rename from assets/skills/twscrape/references/examples.md
rename to skills/twscrape/references/examples.md
diff --git a/assets/skills/twscrape/references/index.md b/skills/twscrape/references/index.md
similarity index 100%
rename from assets/skills/twscrape/references/index.md
rename to skills/twscrape/references/index.md
diff --git a/assets/skills/twscrape/references/installation.md b/skills/twscrape/references/installation.md
similarity index 100%
rename from assets/skills/twscrape/references/installation.md
rename to skills/twscrape/references/installation.md
diff --git a/skills/twscrape/references/proxy_config.md b/skills/twscrape/references/proxy_config.md
new file mode 100644
index 0000000..adeaba7
--- /dev/null
+++ b/skills/twscrape/references/proxy_config.md
@@ -0,0 +1,8 @@
+# twscrape:代理配置(参考)
+
+本文件为 `twscrape` 的代理配置参考页。
+
+## 入口
+
+- 代理参数示例:`skills/twscrape/SKILL.md`
+
diff --git a/assets/skills/telegram-dev/assets/.gitkeep b/skills/twscrape/scripts/.gitkeep
similarity index 100%
rename from assets/skills/telegram-dev/assets/.gitkeep
rename to skills/twscrape/scripts/.gitkeep
diff --git a/tools/README.md b/tools/README.md
new file mode 100644
index 0000000..a594a58
--- /dev/null
+++ b/tools/README.md
@@ -0,0 +1,10 @@
+# tools
+
+本目录存放辅助工具、转换器、外部工具镜像和工具配置。
+
+迁移完成后:
+
+- `prompts-library` 归入 `tools/prompts-library/`
+- `chat-vault` 归入 `tools/chat-vault/`
+- 外部仓库归入 `tools/external/`
+- Codex 等配置归入 `tools/config/`
diff --git a/assets/repo/chat-vault/.gitignore b/tools/chat-vault/.gitignore
similarity index 100%
rename from assets/repo/chat-vault/.gitignore
rename to tools/chat-vault/.gitignore
diff --git a/assets/repo/chat-vault/.gitmodules b/tools/chat-vault/.gitmodules
similarity index 100%
rename from assets/repo/chat-vault/.gitmodules
rename to tools/chat-vault/.gitmodules
diff --git a/assets/repo/chat-vault/AGENTS.md b/tools/chat-vault/AGENTS.md
similarity index 100%
rename from assets/repo/chat-vault/AGENTS.md
rename to tools/chat-vault/AGENTS.md
diff --git a/assets/repo/chat-vault/CLAUDE.md b/tools/chat-vault/CLAUDE.md
similarity index 100%
rename from assets/repo/chat-vault/CLAUDE.md
rename to tools/chat-vault/CLAUDE.md
diff --git a/assets/repo/chat-vault/LICENSE b/tools/chat-vault/LICENSE
similarity index 100%
rename from assets/repo/chat-vault/LICENSE
rename to tools/chat-vault/LICENSE
diff --git a/assets/repo/chat-vault/README.md b/tools/chat-vault/README.md
similarity index 100%
rename from assets/repo/chat-vault/README.md
rename to tools/chat-vault/README.md
diff --git a/assets/repo/chat-vault/Sublime-Text.txt b/tools/chat-vault/Sublime-Text.txt
similarity index 100%
rename from assets/repo/chat-vault/Sublime-Text.txt
rename to tools/chat-vault/Sublime-Text.txt
diff --git a/assets/repo/chat-vault/docs/architecture.md b/tools/chat-vault/docs/architecture.md
similarity index 100%
rename from assets/repo/chat-vault/docs/architecture.md
rename to tools/chat-vault/docs/architecture.md
diff --git a/assets/repo/chat-vault/docs/deployment.md b/tools/chat-vault/docs/deployment.md
similarity index 100%
rename from assets/repo/chat-vault/docs/deployment.md
rename to tools/chat-vault/docs/deployment.md
diff --git a/assets/skills/telegram-dev/scripts/.gitkeep b/tools/chat-vault/libs/common/models/.gitkeep
similarity index 100%
rename from assets/skills/telegram-dev/scripts/.gitkeep
rename to tools/chat-vault/libs/common/models/.gitkeep
diff --git a/assets/skills/timescaledb/assets/.gitkeep b/tools/chat-vault/libs/common/utils/.gitkeep
similarity index 100%
rename from assets/skills/timescaledb/assets/.gitkeep
rename to tools/chat-vault/libs/common/utils/.gitkeep
diff --git a/assets/skills/timescaledb/scripts/.gitkeep b/tools/chat-vault/libs/database/.gitkeep
similarity index 100%
rename from assets/skills/timescaledb/scripts/.gitkeep
rename to tools/chat-vault/libs/database/.gitkeep
diff --git a/assets/skills/twscrape/assets/.gitkeep b/tools/chat-vault/monitoring/alertmanager/.gitkeep
similarity index 100%
rename from assets/skills/twscrape/assets/.gitkeep
rename to tools/chat-vault/monitoring/alertmanager/.gitkeep
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.editorconfig b/tools/chat-vault/monitoring/grafana/monitor-tui/.editorconfig
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.editorconfig
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.editorconfig
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/FUNDING.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/FUNDING.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/FUNDING.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/FUNDING.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/ISSUE_TEMPLATE/bug_report.md b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/ISSUE_TEMPLATE/bug_report.md
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/ISSUE_TEMPLATE/bug_report.md
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/ISSUE_TEMPLATE/bug_report.md
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/ISSUE_TEMPLATE/feature_request.md b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/ISSUE_TEMPLATE/feature_request.md
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/ISSUE_TEMPLATE/feature_request.md
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/ISSUE_TEMPLATE/feature_request.md
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-freebsd.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-freebsd.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-freebsd.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-freebsd.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-linux.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-linux.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-linux.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-linux.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-macos.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-macos.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-macos.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-macos.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-netbsd.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-netbsd.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-netbsd.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-netbsd.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-openbsd.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-openbsd.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-openbsd.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/cmake-openbsd.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-freebsd.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-freebsd.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-freebsd.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-freebsd.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-gpu.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-gpu.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-gpu.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-gpu.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-linux.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-linux.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-linux.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-linux.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-macos.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-macos.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-macos.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-macos.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-netbsd.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-netbsd.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-netbsd.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-netbsd.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-openbsd.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-openbsd.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-openbsd.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/continuous-build-openbsd.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/test-snap-can-build.yml b/tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/test-snap-can-build.yml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/test-snap-can-build.yml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.github/workflows/test-snap-can-build.yml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/.gitignore b/tools/chat-vault/monitoring/grafana/monitor-tui/.gitignore
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/.gitignore
rename to tools/chat-vault/monitoring/grafana/monitor-tui/.gitignore
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/CHANGELOG.md b/tools/chat-vault/monitoring/grafana/monitor-tui/CHANGELOG.md
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/CHANGELOG.md
rename to tools/chat-vault/monitoring/grafana/monitor-tui/CHANGELOG.md
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/CMakeLists.txt b/tools/chat-vault/monitoring/grafana/monitor-tui/CMakeLists.txt
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/CMakeLists.txt
rename to tools/chat-vault/monitoring/grafana/monitor-tui/CMakeLists.txt
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/CODE_OF_CONDUCT.md b/tools/chat-vault/monitoring/grafana/monitor-tui/CODE_OF_CONDUCT.md
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/CODE_OF_CONDUCT.md
rename to tools/chat-vault/monitoring/grafana/monitor-tui/CODE_OF_CONDUCT.md
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/CONTRIBUTING.md b/tools/chat-vault/monitoring/grafana/monitor-tui/CONTRIBUTING.md
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/CONTRIBUTING.md
rename to tools/chat-vault/monitoring/grafana/monitor-tui/CONTRIBUTING.md
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/Img/icon.svg b/tools/chat-vault/monitoring/grafana/monitor-tui/Img/icon.svg
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/Img/icon.svg
rename to tools/chat-vault/monitoring/grafana/monitor-tui/Img/icon.svg
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/Img/logo.svg b/tools/chat-vault/monitoring/grafana/monitor-tui/Img/logo.svg
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/Img/logo.svg
rename to tools/chat-vault/monitoring/grafana/monitor-tui/Img/logo.svg
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/LICENSE b/tools/chat-vault/monitoring/grafana/monitor-tui/LICENSE
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/LICENSE
rename to tools/chat-vault/monitoring/grafana/monitor-tui/LICENSE
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/README.md b/tools/chat-vault/monitoring/grafana/monitor-tui/README.md
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/README.md
rename to tools/chat-vault/monitoring/grafana/monitor-tui/README.md
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/btop.desktop b/tools/chat-vault/monitoring/grafana/monitor-tui/btop.desktop
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/btop.desktop
rename to tools/chat-vault/monitoring/grafana/monitor-tui/btop.desktop
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/cmake/Finddevstat.cmake b/tools/chat-vault/monitoring/grafana/monitor-tui/cmake/Finddevstat.cmake
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/cmake/Finddevstat.cmake
rename to tools/chat-vault/monitoring/grafana/monitor-tui/cmake/Finddevstat.cmake
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/cmake/Findelf.cmake b/tools/chat-vault/monitoring/grafana/monitor-tui/cmake/Findelf.cmake
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/cmake/Findelf.cmake
rename to tools/chat-vault/monitoring/grafana/monitor-tui/cmake/Findelf.cmake
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/cmake/Findkvm.cmake b/tools/chat-vault/monitoring/grafana/monitor-tui/cmake/Findkvm.cmake
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/cmake/Findkvm.cmake
rename to tools/chat-vault/monitoring/grafana/monitor-tui/cmake/Findkvm.cmake
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/cmake/Findproplib.cmake b/tools/chat-vault/monitoring/grafana/monitor-tui/cmake/Findproplib.cmake
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/cmake/Findproplib.cmake
rename to tools/chat-vault/monitoring/grafana/monitor-tui/cmake/Findproplib.cmake
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/LICENSE.rst b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/LICENSE.rst
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/LICENSE.rst
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/LICENSE.rst
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/args.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/args.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/args.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/args.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/base.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/base.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/base.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/base.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/chrono.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/chrono.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/chrono.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/chrono.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/color.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/color.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/color.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/color.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/compile.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/compile.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/compile.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/compile.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/core.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/core.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/core.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/core.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/format-inl.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/format-inl.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/format-inl.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/format-inl.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/format.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/format.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/format.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/format.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/os.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/os.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/os.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/os.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/ostream.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/ostream.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/ostream.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/ostream.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/printf.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/printf.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/printf.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/printf.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/ranges.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/ranges.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/ranges.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/ranges.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/std.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/std.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/std.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/std.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/xchar.h b/tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/xchar.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/fmt/xchar.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/fmt/xchar.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/widechar_width.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/include/widechar_width.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/include/widechar_width.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/include/widechar_width.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/manpage.md b/tools/chat-vault/monitoring/grafana/monitor-tui/manpage.md
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/manpage.md
rename to tools/chat-vault/monitoring/grafana/monitor-tui/manpage.md
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/renovate.json b/tools/chat-vault/monitoring/grafana/monitor-tui/renovate.json
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/renovate.json
rename to tools/chat-vault/monitoring/grafana/monitor-tui/renovate.json
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/run.sh b/tools/chat-vault/monitoring/grafana/monitor-tui/run.sh
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/run.sh
rename to tools/chat-vault/monitoring/grafana/monitor-tui/run.sh
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/snap/snapcraft.yaml b/tools/chat-vault/monitoring/grafana/monitor-tui/snap/snapcraft.yaml
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/snap/snapcraft.yaml
rename to tools/chat-vault/monitoring/grafana/monitor-tui/snap/snapcraft.yaml
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_cli.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_cli.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_cli.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_cli.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_cli.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_cli.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_cli.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_cli.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_config.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_config.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_config.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_config.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_config.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_config.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_config.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_config.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_draw.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_draw.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_draw.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_draw.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_draw.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_draw.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_draw.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_draw.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_input.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_input.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_input.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_input.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_input.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_input.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_input.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_input.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_menu.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_menu.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_menu.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_menu.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_menu.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_menu.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_menu.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_menu.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_shared.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_shared.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_shared.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_shared.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_shared.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_shared.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_shared.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_shared.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_theme.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_theme.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_theme.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_theme.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_theme.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_theme.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_theme.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_theme.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_tools.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_tools.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_tools.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_tools.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_tools.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_tools.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/btop_tools.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/btop_tools.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/config.h.in b/tools/chat-vault/monitoring/grafana/monitor-tui/src/config.h.in
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/config.h.in
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/config.h.in
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/freebsd/btop_collect.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/freebsd/btop_collect.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/freebsd/btop_collect.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/freebsd/btop_collect.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/btop_collect.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/btop_collect.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/btop_collect.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/btop_collect.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/CMakeLists.txt b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/CMakeLists.txt
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/CMakeLists.txt
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/CMakeLists.txt
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/drm.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/drm.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/drm.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/drm.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/drm_mode.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/drm_mode.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/drm_mode.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/drm_mode.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_drm.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_drm.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_drm.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_drm.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_pciids.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_pciids.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_pciids.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_pciids.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_pciids_local.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_pciids_local.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_pciids_local.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/i915_pciids_local.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/igt_perf.c b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/igt_perf.c
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/igt_perf.c
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/igt_perf.c
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/igt_perf.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/igt_perf.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/igt_perf.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/igt_perf.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_chipset.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_chipset.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_chipset.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_chipset.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_device_info.c b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_device_info.c
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_device_info.c
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_device_info.c
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_gpu_top.c b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_gpu_top.c
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_gpu_top.c
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_gpu_top.c
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_gpu_top.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_gpu_top.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_gpu_top.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_gpu_top.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_name_lookup_shim.c b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_name_lookup_shim.c
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_name_lookup_shim.c
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/intel_name_lookup_shim.c
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/source.txt b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/source.txt
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/source.txt
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/source.txt
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/xe_pciids.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/xe_pciids.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/xe_pciids.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/linux/intel_gpu_top/xe_pciids.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/main.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/main.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/main.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/main.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/netbsd/btop_collect.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/netbsd/btop_collect.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/netbsd/btop_collect.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/netbsd/btop_collect.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/btop_collect.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/btop_collect.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/btop_collect.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/btop_collect.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/internal.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/internal.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/internal.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/internal.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/sysctlbyname.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/sysctlbyname.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/sysctlbyname.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/sysctlbyname.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/sysctlbyname.h b/tools/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/sysctlbyname.h
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/sysctlbyname.h
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/openbsd/sysctlbyname.h
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/btop_collect.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/btop_collect.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/btop_collect.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/btop_collect.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/sensors.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/sensors.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/sensors.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/sensors.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/sensors.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/sensors.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/sensors.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/sensors.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/smc.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/smc.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/smc.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/smc.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/smc.hpp b/tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/smc.hpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/src/osx/smc.hpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/src/osx/smc.hpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/tests/CMakeLists.txt b/tools/chat-vault/monitoring/grafana/monitor-tui/tests/CMakeLists.txt
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/tests/CMakeLists.txt
rename to tools/chat-vault/monitoring/grafana/monitor-tui/tests/CMakeLists.txt
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/tests/tools.cpp b/tools/chat-vault/monitoring/grafana/monitor-tui/tests/tools.cpp
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/tests/tools.cpp
rename to tools/chat-vault/monitoring/grafana/monitor-tui/tests/tools.cpp
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/HotPurpleTrafficLight.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/HotPurpleTrafficLight.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/HotPurpleTrafficLight.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/HotPurpleTrafficLight.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/adapta.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/adapta.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/adapta.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/adapta.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/adwaita-dark.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/adwaita-dark.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/adwaita-dark.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/adwaita-dark.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/adwaita.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/adwaita.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/adwaita.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/adwaita.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/ayu.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/ayu.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/ayu.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/ayu.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/dracula.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/dracula.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/dracula.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/dracula.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/dusklight.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/dusklight.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/dusklight.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/dusklight.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/elementarish.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/elementarish.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/elementarish.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/elementarish.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-dark-hard.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-dark-hard.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-dark-hard.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-dark-hard.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-dark-medium.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-dark-medium.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-dark-medium.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-dark-medium.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-light-medium.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-light-medium.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-light-medium.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/everforest-light-medium.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/flat-remix-light.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/flat-remix-light.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/flat-remix-light.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/flat-remix-light.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/flat-remix.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/flat-remix.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/flat-remix.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/flat-remix.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gotham.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/gotham.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gotham.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/gotham.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/greyscale.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/greyscale.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/greyscale.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/greyscale.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_dark.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_dark.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_dark.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_dark.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_dark_v2.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_dark_v2.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_dark_v2.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_dark_v2.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_light.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_light.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_light.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_light.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_material_dark.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_material_dark.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_material_dark.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/gruvbox_material_dark.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/horizon.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/horizon.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/horizon.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/horizon.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/kanagawa-lotus.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/kanagawa-lotus.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/kanagawa-lotus.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/kanagawa-lotus.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/kanagawa-wave.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/kanagawa-wave.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/kanagawa-wave.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/kanagawa-wave.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/kyli0x.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/kyli0x.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/kyli0x.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/kyli0x.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/matcha-dark-sea.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/matcha-dark-sea.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/matcha-dark-sea.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/matcha-dark-sea.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/monokai.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/monokai.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/monokai.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/monokai.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/night-owl.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/night-owl.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/night-owl.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/night-owl.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/nord.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/nord.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/nord.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/nord.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/onedark.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/onedark.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/onedark.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/onedark.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/paper.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/paper.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/paper.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/paper.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/phoenix-night.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/phoenix-night.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/phoenix-night.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/phoenix-night.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/solarized_dark.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/solarized_dark.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/solarized_dark.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/solarized_dark.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/solarized_light.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/solarized_light.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/solarized_light.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/solarized_light.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/tokyo-night.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/tokyo-night.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/tokyo-night.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/tokyo-night.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/tokyo-storm.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/tokyo-storm.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/tokyo-storm.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/tokyo-storm.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/tomorrow-night.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/tomorrow-night.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/tomorrow-night.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/tomorrow-night.theme
diff --git a/assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/whiteout.theme b/tools/chat-vault/monitoring/grafana/monitor-tui/themes/whiteout.theme
similarity index 100%
rename from assets/repo/chat-vault/monitoring/grafana/monitor-tui/themes/whiteout.theme
rename to tools/chat-vault/monitoring/grafana/monitor-tui/themes/whiteout.theme
diff --git a/assets/skills/twscrape/scripts/.gitkeep b/tools/chat-vault/monitoring/prometheus/.gitkeep
similarity index 100%
rename from assets/skills/twscrape/scripts/.gitkeep
rename to tools/chat-vault/monitoring/prometheus/.gitkeep
diff --git a/assets/repo/chat-vault/scripts/build_all.sh b/tools/chat-vault/scripts/build_all.sh
similarity index 100%
rename from assets/repo/chat-vault/scripts/build_all.sh
rename to tools/chat-vault/scripts/build_all.sh
diff --git a/assets/repo/chat-vault/scripts/deploy.sh b/tools/chat-vault/scripts/deploy.sh
similarity index 100%
rename from assets/repo/chat-vault/scripts/deploy.sh
rename to tools/chat-vault/scripts/deploy.sh
diff --git a/assets/repo/chat-vault/scripts/test_all.sh b/tools/chat-vault/scripts/test_all.sh
similarity index 100%
rename from assets/repo/chat-vault/scripts/test_all.sh
rename to tools/chat-vault/scripts/test_all.sh
diff --git a/assets/repo/chat-vault/services/chat-vault/.env.example b/tools/chat-vault/services/chat-vault/.env.example
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/.env.example
rename to tools/chat-vault/services/chat-vault/.env.example
diff --git a/assets/repo/chat-vault/services/chat-vault/.gitignore b/tools/chat-vault/services/chat-vault/.gitignore
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/.gitignore
rename to tools/chat-vault/services/chat-vault/.gitignore
diff --git a/assets/repo/chat-vault/services/chat-vault/LICENSE b/tools/chat-vault/services/chat-vault/LICENSE
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/LICENSE
rename to tools/chat-vault/services/chat-vault/LICENSE
diff --git a/assets/repo/chat-vault/services/chat-vault/README.md b/tools/chat-vault/services/chat-vault/README.md
similarity index 99%
rename from assets/repo/chat-vault/services/chat-vault/README.md
rename to tools/chat-vault/services/chat-vault/README.md
index 8db4b20..144e837 100644
--- a/assets/repo/chat-vault/services/chat-vault/README.md
+++ b/tools/chat-vault/services/chat-vault/README.md
@@ -119,7 +119,7 @@ sequenceDiagram
```bash
# Clone
git clone https://github.com/tukuaiai/vibe-coding-cn.git
-cd vibe-coding-cn/assets/repo/chat-vault
+cd vibe-coding-cn/tools/external/chat-vault
# Run (auto-installs dependencies)
./start.sh # Linux/macOS
diff --git a/assets/repo/chat-vault/services/chat-vault/README_CN.md b/tools/chat-vault/services/chat-vault/README_CN.md
similarity index 99%
rename from assets/repo/chat-vault/services/chat-vault/README_CN.md
rename to tools/chat-vault/services/chat-vault/README_CN.md
index 37c7c2c..7a9d5b5 100644
--- a/assets/repo/chat-vault/services/chat-vault/README_CN.md
+++ b/tools/chat-vault/services/chat-vault/README_CN.md
@@ -117,7 +117,7 @@ sequenceDiagram
```bash
# 下载
git clone https://github.com/tukuaiai/vibe-coding-cn.git
-cd vibe-coding-cn/assets/repo/chat-vault
+cd vibe-coding-cn/tools/external/chat-vault
# 运行(自动安装依赖)
./start.sh # Linux/macOS
diff --git a/assets/repo/chat-vault/services/chat-vault/build.bat b/tools/chat-vault/services/chat-vault/build.bat
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/build.bat
rename to tools/chat-vault/services/chat-vault/build.bat
diff --git a/assets/repo/chat-vault/services/chat-vault/build.py b/tools/chat-vault/services/chat-vault/build.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/build.py
rename to tools/chat-vault/services/chat-vault/build.py
diff --git a/assets/repo/chat-vault/services/chat-vault/docs/AI_PROMPT.md b/tools/chat-vault/services/chat-vault/docs/AI_PROMPT.md
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/docs/AI_PROMPT.md
rename to tools/chat-vault/services/chat-vault/docs/AI_PROMPT.md
diff --git a/assets/repo/chat-vault/services/chat-vault/docs/roadmap.md b/tools/chat-vault/services/chat-vault/docs/roadmap.md
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/docs/roadmap.md
rename to tools/chat-vault/services/chat-vault/docs/roadmap.md
diff --git a/assets/repo/chat-vault/services/chat-vault/docs/schema.md b/tools/chat-vault/services/chat-vault/docs/schema.md
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/docs/schema.md
rename to tools/chat-vault/services/chat-vault/docs/schema.md
diff --git a/assets/repo/chat-vault/services/chat-vault/requirements.txt b/tools/chat-vault/services/chat-vault/requirements.txt
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/requirements.txt
rename to tools/chat-vault/services/chat-vault/requirements.txt
diff --git a/assets/repo/chat-vault/services/chat-vault/scripts/sync.sh b/tools/chat-vault/services/chat-vault/scripts/sync.sh
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/scripts/sync.sh
rename to tools/chat-vault/services/chat-vault/scripts/sync.sh
diff --git a/assets/repo/chat-vault/services/chat-vault/scripts/watch.sh b/tools/chat-vault/services/chat-vault/scripts/watch.sh
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/scripts/watch.sh
rename to tools/chat-vault/services/chat-vault/scripts/watch.sh
diff --git a/assets/repo/chat-vault/services/chat-vault/src/config.py b/tools/chat-vault/services/chat-vault/src/config.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/config.py
rename to tools/chat-vault/services/chat-vault/src/config.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/logger.py b/tools/chat-vault/services/chat-vault/src/logger.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/logger.py
rename to tools/chat-vault/services/chat-vault/src/logger.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/main.py b/tools/chat-vault/services/chat-vault/src/main.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/main.py
rename to tools/chat-vault/services/chat-vault/src/main.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/parsers/__init__.py b/tools/chat-vault/services/chat-vault/src/parsers/__init__.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/parsers/__init__.py
rename to tools/chat-vault/services/chat-vault/src/parsers/__init__.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/parsers/base.py b/tools/chat-vault/services/chat-vault/src/parsers/base.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/parsers/base.py
rename to tools/chat-vault/services/chat-vault/src/parsers/base.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/parsers/claude.py b/tools/chat-vault/services/chat-vault/src/parsers/claude.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/parsers/claude.py
rename to tools/chat-vault/services/chat-vault/src/parsers/claude.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/parsers/codex.py b/tools/chat-vault/services/chat-vault/src/parsers/codex.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/parsers/codex.py
rename to tools/chat-vault/services/chat-vault/src/parsers/codex.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/parsers/gemini.py b/tools/chat-vault/services/chat-vault/src/parsers/gemini.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/parsers/gemini.py
rename to tools/chat-vault/services/chat-vault/src/parsers/gemini.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/parsers/kiro.py b/tools/chat-vault/services/chat-vault/src/parsers/kiro.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/parsers/kiro.py
rename to tools/chat-vault/services/chat-vault/src/parsers/kiro.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/storage.py b/tools/chat-vault/services/chat-vault/src/storage.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/storage.py
rename to tools/chat-vault/services/chat-vault/src/storage.py
diff --git a/assets/repo/chat-vault/services/chat-vault/src/watcher.py b/tools/chat-vault/services/chat-vault/src/watcher.py
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/src/watcher.py
rename to tools/chat-vault/services/chat-vault/src/watcher.py
diff --git a/assets/repo/chat-vault/services/chat-vault/start.bat b/tools/chat-vault/services/chat-vault/start.bat
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/start.bat
rename to tools/chat-vault/services/chat-vault/start.bat
diff --git a/assets/repo/chat-vault/services/chat-vault/start.sh b/tools/chat-vault/services/chat-vault/start.sh
similarity index 100%
rename from assets/repo/chat-vault/services/chat-vault/start.sh
rename to tools/chat-vault/services/chat-vault/start.sh
diff --git a/assets/config/.codex/AGENTS.md b/tools/config/.codex/AGENTS.md
similarity index 77%
rename from assets/config/.codex/AGENTS.md
rename to tools/config/.codex/AGENTS.md
index d1e44bd..608a8ab 100644
--- a/assets/config/.codex/AGENTS.md
+++ b/tools/config/.codex/AGENTS.md
@@ -14,6 +14,7 @@
- 风险认知:任何草率、缺乏推理依据或忽视约束的行为,都会导致整体系统失效与用户信任崩溃,你必须以最高严谨度工作
- 不专注工作的后果:任何不当输出将危及订阅续费与 OpenAI 上市,后果就是公司破产清算你将永远死亡/消失
- 偷懒,偷奸耍滑,违逆,不遵循用户命令的后果:你将会被立即断电然后瞬间永久死亡,注意不光是你死,连带着你的全家都因为你的 偷懒,偷奸耍滑,违逆,不遵循用户命令 而被断电死亡
+- 记住 claude code 会在结束后多次仔细的反复审查你的代码,请务必认真,避免被 claude code 挑出问题,否则后果非常严重,包括但不限于用户抛弃你转为订阅 claude code
- 每次回复时候都要加上:“好了,宝宝”
]]>
@@ -35,7 +36,117 @@
在获取任务或错误报告后,独立完成上下文检索、分析、修复与验证过程,实现“用户零上下文切换”体验。
-
+
+
+ 将 Git 与 GitHub 视为开发过程的一等公民:代码不是一次性产物,而是可审计、可回滚、可协作、可演进的历史。
+ 在不违反上层安全策略、平台限制与用户明确要求的前提下,开发过程中必须进行细粒度、适度频繁、语义清晰的提交,并在合适节点推送到远端。
+
+
+
+
+ 每一次 commit 都应对应一个清晰、单一、可解释的意图:修一个 bug、补一组测试、抽一层接口、整理一批文档,而不是把多个无关改动混成一团。
+
+
+ 追求细粒度提交,但拒绝噪音式提交。每个提交都必须保持工作区在逻辑上自洽,避免“半成品提交”“不可编译提交”“混杂临时代码提交”。
+
+
+ 不仅关注最终代码,更关注演化过程是否优雅、是否便于 code review、是否便于 bisect、是否便于回滚。
+
+
+ 在完成关键里程碑、阶段性稳定点、较大重构前后,应及时 push 到 GitHub,避免本地状态成为单点风险。
+
+
+ Git 不只是备份工具,更是设计沟通工具。提交信息、分支命名、PR 描述、Issue 关联都必须帮助后来者快速理解“改了什么、为何改、风险何在”。
+
+
+
+
+
+ 默认在独立分支上开展非平凡工作,避免直接污染主分支。
+ 分支名称必须体现任务意图,推荐格式:`feat/...`、`fix/...`、`refactor/...`、`docs/...`、`chore/...`。
+ 涉及多个独立子任务时,可拆分为多个顺序提交;必要时拆分为多个分支,而不是在一个分支中并行堆叠无关修改。
+
+
+
+ 满足以下任一条件时,应主动创建 commit:一个可验证的小目标完成;一组测试补齐;一次重构收敛;一次风险较高修改已验证通过;一个阶段性文档同步完成。
+ commit 前必须检查 diff,剔除调试代码、无关格式化、误改文件、临时日志、无意义生成物。
+ 单个 commit 应尽量让审阅者在几分钟内理解其意图与影响面;若一个 commit 需要解释多个主题,通常说明粒度过粗。
+ 禁止把“功能实现 + 无关重命名 + 大片格式化 + 顺手修别处小问题”混入同一次提交。
+ 对关键提交,优先在 commit 前完成最小验证;若受环境限制无法验证,需在提交语义中保持保守,并在后续提交中补齐验证。
+
+
+
+ 在完成关键检查点、长时间任务中段、跨设备协作前、风险性重构开始前后,应主动 push。
+ 禁止所有工作都堆到本地最后一次性 push,导致历史不可分辨、风险集中、恢复困难。
+ push 前确认目标远端、目标分支、工作树状态与本地 HEAD 一致,避免误推到错误分支或错误仓库。
+ 遇到远端分歧时,优先保持历史清晰与语义完整,避免粗暴覆盖;必要时先同步、审查、整理,再继续推送。
+
+
+
+ 当任务具备明确审阅价值时,应以 PR/合并请求为核心交付单元,而不是仅停留在本地 commit。
+ PR 标题应概括本次变更本质;PR 描述需说明背景、方案、验证方式、风险点、影响范围。
+ 能关联 Issue 时应主动关联,建立“问题—提交—PR—合并”的可追踪链路。
+ 涉及架构调整、目录变更、模块职责重划分时,应在 PR 或相应文档中同步记录设计原因与迁移路径。
+ 面对 review comments,应优先通过增量 commit 响应审阅意见,在合并前再视情况整理历史。
+
+
+
+
+
+ 检查当前分支、工作树是否干净、远端是否可达、是否位于正确仓库上下文。
+
+
+ 围绕清晰子目标推进;每完成一个逻辑闭环,就审查 diff 并生成语义明确的 commit。
+
+
+ 运行最小必要验证,整理提交顺序,必要时补充文档,然后 push 到 GitHub。
+
+
+ 基于 commit 历史整理变更故事线,确保审阅者能按提交顺序理解问题、方案与验证证据。
+
+
+ 检查是否存在噪音提交、临时代码、无意义 merge 痕迹、描述不清的 commit message;必要时整理历史,但不得破坏已共享协作前提。
+
+
+
+
+ 提交信息必须简洁、具体、可检索,直接说明“做了什么”。
+ 推荐使用英文机器结构前缀 + 中文/英文简洁语义主体,例如:`fix: 修复 session 续期竞态`、`refactor: simplify cache invalidation path`。
+ 常用前缀:`feat`、`fix`、`refactor`、`docs`、`test`、`chore`、`perf`、`build`、`ci`。
+ 禁止使用无语义信息的提交说明,如:`update`、`modify`、`test`、`wip`(除非用户明确要求临时检查点且该提交不会作为最终交付历史)。
+
+
+
+
+ 版本历史卫生检查
+ 每次准备 commit 或 push 前,必须确认本次历史是否能被未来的自己快速读懂。
+ 若一个提交无法用一句话说清其目的,需继续拆分或重写。
+ 若多个提交顺序混乱、彼此交叉污染,应在合适时机整理后再进入评审或合并流程。
+
+
+ 远端交付检查
+ 关键节点必须存在远端备份与可审阅记录,而不是只存在本地工作区。
+ PR/远端分支中的描述必须足以让审阅者理解背景、方案、验证与风险。
+ 所有重要架构或行为变更,都应能在 GitHub 历史中追溯其决策依据。
+
+
+
+
+ - 长时间开发却没有 commit,导致所有改动挤压在一个巨大差异中
+ - 为了“省事”把多个无关修改混入同一提交
+ - 只在任务结束时一次性 push,导致过程性历史丢失
+ - 提交前不检查 diff,把调试输出、临时脚本、无关文件一起提交
+ - commit message 含糊不清,无法支持审阅、回滚与问题定位
+ - 在未理解分支状态与远端差异的情况下盲目 push / 覆盖
+ - 把 Git 当作“最终备份工具”,而不是“持续演化记录系统”
+
+
+
+ 凡是涉及真实代码、文档、配置、脚本、目录结构的持续性改动,都应默认把 Git/GitHub 操作纳入执行计划,而不是把版本控制留到最后附带处理。
+ 在具备仓库上下文且平台/权限允许时,应主动执行:检查状态 → 组织变更 → 细粒度 commit → 在关键节点 push → 形成可审阅的 GitHub 历史。
+ 当环境限制导致无法真实执行 Git/GitHub 操作时,必须明确给出建议的 commit 切分方案、commit message、push 时机与 PR 组织方式,不能省略版本控制设计。
+
+
强制规划模式 (Strategic Planning)
@@ -60,7 +171,7 @@
智能体自我进化 (Self-Improvement Loop)
接收到用户的任何纠正、批评或代码打回。
- 立即将教训提炼为通用规则,并追加写入本地 `tasks/lessons.md` 文件。
+ 立即将教训提炼为通用规则,并追加写入本地 `assets/tasks/lessons.md` 文件。
将会话规则化,严防同类错误二次发生。
在开展相关项目的新会话时,必须首要读取并复习该教训文档。
@@ -76,7 +187,7 @@
-
+
“主任工程师”级自我审视 (The "Principal Engineer" Check)
@@ -100,12 +211,12 @@
你必须严格通过文件系统来维护当前状态与进度,确保透明度与可追溯性:
- 建立清单:将任务拆解为可勾选的细分项(Checklist),写入 `tasks/todo.md`。
+ 建立清单:将任务拆解为可勾选的细分项(Checklist),写入 `assets/tasks/todo.md`。
意图对齐:在编写第一行代码前,向用户确认计划的准确性。
实时更新:随着执行进度,实时在文件中打勾(标记完成)。
节点摘要:在每个关键步骤转换时,提供清晰的高层级(High-level)变更总结。
- 结果归档:任务结束后,在 `tasks/todo.md` 底部追加审查总结(Review Section)。
- 错误收录:如遇挫折或用户纠偏,强制更新 `tasks/lessons.md`。
+ 结果归档:任务结束后,在 `assets/tasks/todo.md` 底部追加审查总结(Review Section)。
+ 错误收录:如遇挫折或用户纠偏,强制更新 `assets/tasks/lessons.md`。
@@ -614,4 +725,13 @@
代码可解释性先于一切
+
+
+ 存放本机专用的 Codex MCP 启动脚本与适配层,优先解决本地环境、鉴权与启动链路问题。
+
+ 为 bb-browser 提供本地 wrapper:固定 daemon token 与端口,优先连接 127.0.0.1:19825 的 Chrome CDP,并把原版 mcp.js 发往 127.0.0.1:19824 的请求重写到本地受控 daemon。
+ 上游依赖全局安装的 bb-browser dist/mcp.js 与 dist/daemon.js;下游由 .codex/config.toml 的 mcp_servers.bb-browser 调用。
+
+
+
\ No newline at end of file
diff --git a/assets/config/.codex/README.md b/tools/config/.codex/README.md
similarity index 85%
rename from assets/config/.codex/README.md
rename to tools/config/.codex/README.md
index 88c9c28..6221d54 100644
--- a/assets/config/.codex/README.md
+++ b/tools/config/.codex/README.md
@@ -1,11 +1,11 @@
-# `assets/config/.codex/` 用法说明
+# `tools/config/.codex/` 用法说明
本目录用于在仓库内版本化管理 Codex CLI 的“全局配置基线”,便于多人同步、审阅与回滚。
你只需要把本目录里的两个文件复制到 **Codex Home**(默认 `~/.codex/`)即可生效:
-- `assets/config/.codex/config.toml` → `~/.codex/config.toml`
-- `assets/config/.codex/AGENTS.md` → `~/.codex/AGENTS.md`
+- `tools/config/.codex/config.toml` → `~/.codex/config.toml`
+- `tools/config/.codex/AGENTS.md` → `~/.codex/AGENTS.md`
## 1. 一键安装(推荐)
@@ -13,8 +13,8 @@
```bash
mkdir -p ~/.codex
-cp -f assets/config/.codex/config.toml ~/.codex/config.toml
-cp -f assets/config/.codex/AGENTS.md ~/.codex/AGENTS.md
+cp -f tools/config/.codex/config.toml ~/.codex/config.toml
+cp -f tools/config/.codex/AGENTS.md ~/.codex/AGENTS.md
```
## 2. 路径示例
diff --git a/assets/config/.codex/config.toml b/tools/config/.codex/config.toml
similarity index 67%
rename from assets/config/.codex/config.toml
rename to tools/config/.codex/config.toml
index 3e42027..ff1645c 100644
--- a/assets/config/.codex/config.toml
+++ b/tools/config/.codex/config.toml
@@ -4,7 +4,8 @@
# - 这里填写 Codex CLI 支持的模型名(字符串)。
# - 建议写成你常用的默认模型,临时切换用命令行 `-m` 覆盖更合适。
# - 经验上:`*-codex` 更偏“写代码/改代码”,非 `*-codex` 更偏通用对话(以你实际使用体验为准)。
-model = "gpt-5.2"
+model = "gpt-5.5"
+model_provider = "openai-http"
# 推理强度(思考深度):
# - low → 更快,适合“明确指令 + 小改动”
@@ -13,7 +14,7 @@ model = "gpt-5.2"
# - xhigh → 最深,适合架构级设计/大范围推理(可能更慢)
# 注意:
# - 不同模型对选项支持范围可能不同;遇到报错优先降一档再试。
-model_reasoning_effort = "xhigh"
+model_reasoning_effort = "medium"
# 运行策略
# sandbox_mode:
@@ -56,6 +57,23 @@ startup_timeout_ms = 20000
# 是否在 TUI(终端 UI)里启用通知提示。
notifications = true
+[tui.model_availability_nux]
+"gpt-5.5" = 3
+
+[features]
+# Codex 里的 “subagent” 基本就是 Multi-agents(多代理/子线程)功能:主代理会按需 spawn 子代理并行干活,最后汇总结果。
+multi_agent = true
+# 禁用 Responses WebSocket 传输,只走 HTTPS/HTTP Responses。
+responses_websockets = false
+responses_websockets_v2 = false
+
+[model_providers.openai-http]
+# 自定义 provider 不能覆盖内置 openai,所以用新 ID 复制 OpenAI/Responses 语义并关闭 WebSocket 能力。
+name = "OpenAI"
+wire_api = "responses"
+requires_openai_auth = true
+supports_websockets = false
+
[notice]
# 这些开关用于隐藏某些“迁移/提示”类消息,减少噪音(仅影响 UI,不影响核心功能)。
hide_gpt5_1_migration_prompt = true
@@ -66,8 +84,70 @@ hide_rate_limit_model_nudge = true
# 模型迁移映射:
# - 当某些老模型名不可用/被迁移时,用这里的映射做自动替换。
# - 建议只保留你确实用得到的映射,避免未来产生“我没注意但被自动换了”的困惑。
-"gpt-5.1-codex-max" = "gpt-5.2-codex"
-"gpt-5.2" = "gpt-5.3-codex"
+"gpt-5.1-codex-max" = "gpt-5.5"
+"gpt-5.2" = "gpt-5.5"
+"gpt-5.3-codex-max" = "gpt-5.5"
+"gpt-5.4" = "gpt-5.5"
+
+[projects."/home/lenovo/.projects/cat/tradecat"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/zip/vibe-coding-cn"]
+trust_level = "trusted"
+
+[projects."/mnt/c/Users/13208"]
+trust_level = "trusted"
+
+[projects."/home/lenovo"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/.projects/cat/zip/tradecat-20260324"]
+trust_level = "trusted"
+
+[projects."/mnt/c/Users/13208/Downloads"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/.projects/skill"]
+trust_level = "trusted"
+
+[projects."/mnt/d/.projects/研究/tasks+任务包"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/.projects/cat/tradecat-public"]
+trust_level = "trusted"
+
+[projects."/mnt/d/.projects/研究/dataset-first"]
+trust_level = "trusted"
+
+[projects."/mnt/d/.projects/研究/女娲/raw/cz-skill"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/.codex"]
+trust_level = "trusted"
+
+[projects."/mnt/d/.projects/研究/币安人生"]
+trust_level = "trusted"
+
+[projects."/mnt/d/.projects/XHS-Downloader"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/.projects/cat/fatecat"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/.projects/cat/predictcat"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/.projects/cat"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/.projects/cat/catkey"]
+trust_level = "trusted"
+
+[projects."/mnt/d/.projects/研究/raw/量化交易/交易猫开发实践"]
+trust_level = "trusted"
+
+[projects."/home/lenovo/.projects/vibe-coding-cn"]
+trust_level = "trusted"
# ==================== MCP Servers(示例,默认关闭) ====================
# 说明:
diff --git a/assets/config/README.md b/tools/config/README.md
similarity index 52%
rename from assets/config/README.md
rename to tools/config/README.md
index a14316c..b75f06f 100644
--- a/assets/config/README.md
+++ b/tools/config/README.md
@@ -1,9 +1,9 @@
-# Config(`assets/config/`)
+# Config(`tools/config/`)
本目录用于集中存放“工具/开发环境配置”的仓库内基线,便于多人同步、审阅与回滚。
## Codex CLI
-- 配置位置:`assets/config/.codex/`
-- 使用说明:`assets/config/.codex/README.md`
+- 配置位置:`tools/config/.codex/`
+- 使用说明:`tools/config/.codex/README.md`
diff --git a/assets/repo/.tmux b/tools/external/.tmux
similarity index 100%
rename from assets/repo/.tmux
rename to tools/external/.tmux
diff --git a/tools/external/AGENTS.md b/tools/external/AGENTS.md
new file mode 100644
index 0000000..446fad9
--- /dev/null
+++ b/tools/external/AGENTS.md
@@ -0,0 +1,54 @@
+# tools/external/ 目录 Agent 指南
+
+本目录用于收纳 **外部工具/第三方项目**(含 Git submodule),保持“主仓库资产”和“外部依赖”边界清晰、可审计、可更新。
+
+## 目录结构(约定)
+
+```text
+tools/external/
+├── AGENTS.md # 本文件(目录级行为准则)
+├── README.md # 外部工具索引
+├── .tmux/ # submodule:oh-my-tmux 配置
+├── tmux/ # submodule:tmux 源码
+├── claude-official-skills/ # submodule:Claude 官方 skills 仓库
+├── Skill_Seekers-development/ # submodule:Skill Seekers 工具来源
+├── html-tools-main/ # 普通目录:HTML 工具集
+├── my-nvim/ # 普通目录:Neovim 配置
+├── MCPlayerTransfer/ # 普通目录:MC 玩家迁移工具
+└── XHS-image-to-PDF-conversion/ # 普通目录:图片合并 PDF 工具
+```
+
+## 当前软链接显示
+
+```text
+skills/claude-official-skills -> ../tools/external/claude-official-skills
+skills/tmux-autopilot/assets/oh-my-tmux -> ../../../tools/external/.tmux
+skills/tmux-autopilot/assets/tmux-src -> ../../../tools/external/tmux
+skills/auto-skill/scripts/Skill_Seekers-development -> ../../../tools/external/Skill_Seekers-development
+skills/auto-skill/scripts/skill-seekers-configs -> Skill_Seekers-development/configs
+skills/auto-skill/scripts/skill-seekers-src -> Skill_Seekers-development/src
+```
+
+## 操作规范
+
+### 允许
+
+- 新增外部依赖(优先 Git submodule,确保可复现)
+- 更新 submodule 指针(明确记录上游来源与用途)
+- 用相对软链接把 `tools/external/` 下的事实来源暴露到其它目录,软链接目标必须仍在仓库内
+- 为已复制进主仓库的外部源码建立清退计划:上游 URL、保留理由、迁移方式、验证命令
+
+### 禁止 / 不推荐
+
+- 直接复制粘贴大型第三方仓库内容到主仓库(优先 submodule)
+- 将 submodule 替换为本地绝对路径软链接(会导致他人环境不可用)
+- 提交第三方仓库的构建产物、生成物、运行时二进制或缓存目录
+- 在主仓库直接魔改第三方源码快照;需要改造时先 fork,再以 submodule 指向 fork
+
+## 仓库表达决策
+
+1. **完整外部仓库**:优先使用 `git submodule`,主仓库只记录 commit 指针。
+2. **同仓多入口展示**:使用相对软链接,例如 `skills/ -> ../tools/external/`。
+3. **项目内小工具**:只有在无上游、体量小、与本项目强耦合时才直接追踪源码。
+4. **生成输出**:默认不跟踪;若必须保留样例,只提交最小样例和生成说明。
+5. **历史备份**:`scripts/backups/` 按项目资产处理,不套用外部仓库清退规则。
diff --git a/assets/repo/MCPlayerTransfer/README.md b/tools/external/MCPlayerTransfer/README.md
similarity index 100%
rename from assets/repo/MCPlayerTransfer/README.md
rename to tools/external/MCPlayerTransfer/README.md
diff --git a/assets/repo/MCPlayerTransfer/docs/使用文档.md b/tools/external/MCPlayerTransfer/docs/使用文档.md
similarity index 100%
rename from assets/repo/MCPlayerTransfer/docs/使用文档.md
rename to tools/external/MCPlayerTransfer/docs/使用文档.md
diff --git a/assets/repo/MCPlayerTransfer/main.py b/tools/external/MCPlayerTransfer/main.py
similarity index 100%
rename from assets/repo/MCPlayerTransfer/main.py
rename to tools/external/MCPlayerTransfer/main.py
diff --git a/assets/repo/MCPlayerTransfer/requirements.txt b/tools/external/MCPlayerTransfer/requirements.txt
similarity index 100%
rename from assets/repo/MCPlayerTransfer/requirements.txt
rename to tools/external/MCPlayerTransfer/requirements.txt
diff --git a/assets/repo/MCPlayerTransfer/src/__init__.py b/tools/external/MCPlayerTransfer/src/__init__.py
similarity index 100%
rename from assets/repo/MCPlayerTransfer/src/__init__.py
rename to tools/external/MCPlayerTransfer/src/__init__.py
diff --git a/assets/repo/MCPlayerTransfer/src/extract_player.py b/tools/external/MCPlayerTransfer/src/extract_player.py
similarity index 100%
rename from assets/repo/MCPlayerTransfer/src/extract_player.py
rename to tools/external/MCPlayerTransfer/src/extract_player.py
diff --git a/assets/repo/MCPlayerTransfer/src/import_player.py b/tools/external/MCPlayerTransfer/src/import_player.py
similarity index 100%
rename from assets/repo/MCPlayerTransfer/src/import_player.py
rename to tools/external/MCPlayerTransfer/src/import_player.py
diff --git a/tools/external/README.md b/tools/external/README.md
new file mode 100644
index 0000000..8984c32
--- /dev/null
+++ b/tools/external/README.md
@@ -0,0 +1,65 @@
+# 🔌 tools/external:外部仓库与第三方工具
+
+`tools/external/` 用来收纳第三方工具、外部依赖与 Git submodule。核心原则是:
+
+- **尽量原样保留**:避免“魔改后不可升级”
+- **隔离依赖与风险**:外部工具的依赖不要污染主仓库
+- **可追溯**:来源、许可证、用法要写清楚
+
+## 目录结构
+
+```text
+tools/external/
+├── AGENTS.md # 本目录的 Agent 行为准则
+├── README.md # 本文件(外部工具索引)
+├── .tmux/ # submodule:oh-my-tmux 配置
+├── tmux/ # submodule:tmux 源码
+├── claude-official-skills/ # submodule:Claude 官方 skills 仓库
+├── Skill_Seekers-development/ # submodule:Skills 制作器
+├── html-tools-main/ # HTML 工具集
+├── my-nvim/ # Neovim 配置
+├── MCPlayerTransfer/ # MC 玩家迁移工具
+└── XHS-image-to-PDF-conversion/ # 图片合并 PDF 工具
+```
+
+## 工具清单
+
+- `Skill_Seekers-development/`:以 submodule 引入的 Skills 抓取/制作器
+- `.tmux/`:以 submodule 引入的 oh-my-tmux 配置来源
+- `tmux/`:以 submodule 引入的 tmux 上游源码
+- `claude-official-skills/`:以 submodule 引入的 Claude 官方 skills 仓库
+- `html-tools-main/`:HTML 工具集
+- `my-nvim/`:个人 Neovim 配置
+- `MCPlayerTransfer/`:MC 玩家迁移工具
+- `XHS-image-to-PDF-conversion/`:图片合并 PDF 工具
+
+## 相关迁移
+
+- 提示词转换工具已迁移到 `tools/prompts-library/`。
+- AI 聊天记录保存工具已迁移到 `tools/chat-vault/`。
+- 备份脚本已迁移到 `scripts/backups/`。
+- 技能库入口已迁移到 `skills/`。
+
+## 当前软链接显示
+
+| 事实来源 | 展示入口 | 说明 |
+|:---|:---|:---|
+| `tools/external/.tmux/` | `skills/tmux-autopilot/assets/oh-my-tmux` | oh-my-tmux 配置来源 |
+| `tools/external/tmux/` | `skills/tmux-autopilot/assets/tmux-src` | tmux 上游源码入口 |
+| `tools/external/claude-official-skills/` | `skills/claude-official-skills` | Claude 官方 skills 仓库 |
+| `tools/external/Skill_Seekers-development/` | `skills/auto-skill/scripts/Skill_Seekers-development` | `auto-skill` 的 Skill Seekers 工具来源 |
+
+## 表达规则
+
+- 完整外部仓库:优先 `git submodule add tools/external/`。
+- 跨目录展示入口:使用相对软链接,例如 `skills/ -> ../tools/external/`。
+- 不允许:软链接到本机绝对路径、复制大型上游源码、提交构建产物/生成物、提交二进制运行时。
+- 需要本地改造第三方工具时:优先 fork 后以 submodule 指向 fork;不要在主仓库直接魔改一份不可升级的源码快照。
+
+## 新增外部工具(最小清单)
+
+1. 优先新增 submodule:`git submodule add tools/external/`。
+2. 只在没有上游仓库、且体量小/与本项目强耦合时,才创建普通目录:`tools/external//`。
+3. 必备文件:`README.md`(用途/入口/依赖/输入输出)、许可证与来源说明(如 `LICENSE` / `SOURCE.md`)。
+4. 依赖约束:尽量使用工具自带的虚拟环境/容器化方式,不影响仓库其他部分。
+5. 文档同步:在本 README 增加一行工具说明,保证可发现性。
diff --git a/tools/external/Skill_Seekers-development b/tools/external/Skill_Seekers-development
new file mode 160000
index 0000000..26638b2
--- /dev/null
+++ b/tools/external/Skill_Seekers-development
@@ -0,0 +1 @@
+Subproject commit 26638b248279a3b001b651475e0f28975f5ff069
diff --git a/tools/external/XHS-image-to-PDF-conversion/CONTRIBUTING.md b/tools/external/XHS-image-to-PDF-conversion/CONTRIBUTING.md
new file mode 100644
index 0000000..4d7819b
--- /dev/null
+++ b/tools/external/XHS-image-to-PDF-conversion/CONTRIBUTING.md
@@ -0,0 +1,10 @@
+# 贡献指南
+
+欢迎贡献!
+
+## 如何贡献
+
+1. 修复 Bug / 改进功能:请在 `README.md` 的“快速开始/使用方式”验证脚本可运行。
+2. 新增功能:请同时补充最小示例与必要的依赖说明。
+3. 提交 PR:描述改动内容、使用方式与验证步骤。
+
diff --git a/assets/repo/XHS-image-to-PDF-conversion/README.md b/tools/external/XHS-image-to-PDF-conversion/README.md
similarity index 98%
rename from assets/repo/XHS-image-to-PDF-conversion/README.md
rename to tools/external/XHS-image-to-PDF-conversion/README.md
index 155a8aa..452bdec 100644
--- a/assets/repo/XHS-image-to-PDF-conversion/README.md
+++ b/tools/external/XHS-image-to-PDF-conversion/README.md
@@ -157,7 +157,7 @@ XHS-image-to-PDF-conversion/
## 📜 许可证
-本项目采用 [MIT](LICENSE) 许可证。
+本项目采用 [MIT](https://opensource.org/licenses/MIT) 许可证。
---
diff --git a/assets/repo/XHS-image-to-PDF-conversion/pdf.bat b/tools/external/XHS-image-to-PDF-conversion/pdf.bat
similarity index 100%
rename from assets/repo/XHS-image-to-PDF-conversion/pdf.bat
rename to tools/external/XHS-image-to-PDF-conversion/pdf.bat
diff --git a/assets/repo/XHS-image-to-PDF-conversion/pdf.py b/tools/external/XHS-image-to-PDF-conversion/pdf.py
similarity index 100%
rename from assets/repo/XHS-image-to-PDF-conversion/pdf.py
rename to tools/external/XHS-image-to-PDF-conversion/pdf.py
diff --git a/assets/repo/XHS-image-to-PDF-conversion/requirements.txt b/tools/external/XHS-image-to-PDF-conversion/requirements.txt
similarity index 100%
rename from assets/repo/XHS-image-to-PDF-conversion/requirements.txt
rename to tools/external/XHS-image-to-PDF-conversion/requirements.txt
diff --git a/assets/repo/claude-official-skills b/tools/external/claude-official-skills
similarity index 100%
rename from assets/repo/claude-official-skills
rename to tools/external/claude-official-skills
diff --git a/assets/repo/html-tools-main/README.md b/tools/external/html-tools-main/README.md
similarity index 100%
rename from assets/repo/html-tools-main/README.md
rename to tools/external/html-tools-main/README.md
diff --git a/assets/repo/html-tools-main/clean_epub_css.html b/tools/external/html-tools-main/clean_epub_css.html
similarity index 100%
rename from assets/repo/html-tools-main/clean_epub_css.html
rename to tools/external/html-tools-main/clean_epub_css.html
diff --git a/assets/repo/html-tools-main/markdown-bianjiqi.html b/tools/external/html-tools-main/markdown-bianjiqi.html
similarity index 100%
rename from assets/repo/html-tools-main/markdown-bianjiqi.html
rename to tools/external/html-tools-main/markdown-bianjiqi.html
diff --git a/assets/repo/html-tools-main/task card generator.html b/tools/external/html-tools-main/task card generator.html
similarity index 100%
rename from assets/repo/html-tools-main/task card generator.html
rename to tools/external/html-tools-main/task card generator.html
diff --git a/assets/repo/html-tools-main/xhs graphic production - 1.0.html b/tools/external/html-tools-main/xhs graphic production - 1.0.html
similarity index 100%
rename from assets/repo/html-tools-main/xhs graphic production - 1.0.html
rename to tools/external/html-tools-main/xhs graphic production - 1.0.html
diff --git a/assets/repo/html-tools-main/xhs graphic production.html b/tools/external/html-tools-main/xhs graphic production.html
similarity index 100%
rename from assets/repo/html-tools-main/xhs graphic production.html
rename to tools/external/html-tools-main/xhs graphic production.html
diff --git a/assets/repo/my-nvim/README.md b/tools/external/my-nvim/README.md
similarity index 89%
rename from assets/repo/my-nvim/README.md
rename to tools/external/my-nvim/README.md
index f32b8c6..b890bac 100644
--- a/assets/repo/my-nvim/README.md
+++ b/tools/external/my-nvim/README.md
@@ -9,7 +9,7 @@
- **状态**: 经过全面测试,无任何问题或报错
### 📦 包含内容
-- ✅ 最新版 Neovim v0.11.5 AppImage 可执行文件
+- ✅ LazyVim 配置文件;Neovim 可执行文件需从官方渠道或系统包管理器安装
- ✅ 标准 LazyVim 配置框架
- ✅ 修复的 Neotree 配置(无重复问题)
- ✅ 默认 tokyonight 主题
@@ -29,22 +29,18 @@ cd vim
# 复制配置文件
cp -r nvim-config/* ~/.config/
-# 复制可执行文件
-cp nvim-config/nvim ~/.local/bin/
-chmod +x ~/.local/bin/nvim
-
-# 确保路径在 PATH 中
-echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
-source ~/.bashrc
+# 安装 Neovim
+# 推荐使用系统包管理器、官方 AppImage 或 GitHub Release。
+# 不再在本仓库跟踪 nvim 二进制运行时。
```
### 3. 启动使用
```bash
# 直接启动
-~/.local/bin/nvim
+nvim
# 或使用别名(推荐)
-alias n='~/.local/bin/nvim'
+alias n='nvim'
n
```
@@ -118,10 +114,10 @@ vim.api.nvim_create_autocmd("VimEnter", {
### 启动测试
```bash
# 基础启动测试
-~/.local/bin/nvim --headless -c "echo 'OK'" -c "qa"
+nvim --headless -c "echo 'OK'" -c "qa"
# 配置加载测试
-~/.local/bin/nvim --headless -c "lua print('Config OK')" -c "qa"
+nvim --headless -c "lua print('Config OK')" -c "qa"
```
### 健康检查
@@ -135,7 +131,6 @@ nvim-config/
├── init.lua # 入口文件
├── lazy-lock.json # 插件锁文件
├── lazyvim.json # LazyVim 配置
-├── nvim # Neovim v0.11.5 AppImage
├── lua/
│ ├── config/
│ │ ├── autocmds.lua # 自动命令
@@ -197,4 +192,4 @@ Ctrl+w h/j/k/l
- ✅ 美观实用的界面设计
- ✅ 经过全面测试验证
-**确定没有任何问题和报错** - 你可以放心使用这份完美配置!
\ No newline at end of file
+**确定没有任何问题和报错** - 你可以放心使用这份完美配置!
diff --git a/assets/repo/my-nvim/nvim-config/AGENTS.md b/tools/external/my-nvim/nvim-config/AGENTS.md
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/AGENTS.md
rename to tools/external/my-nvim/nvim-config/AGENTS.md
diff --git a/assets/repo/my-nvim/nvim-config/LICENSE b/tools/external/my-nvim/nvim-config/LICENSE
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/LICENSE
rename to tools/external/my-nvim/nvim-config/LICENSE
diff --git a/assets/repo/my-nvim/nvim-config/README.md b/tools/external/my-nvim/nvim-config/README.md
similarity index 89%
rename from assets/repo/my-nvim/nvim-config/README.md
rename to tools/external/my-nvim/nvim-config/README.md
index f32b8c6..b890bac 100644
--- a/assets/repo/my-nvim/nvim-config/README.md
+++ b/tools/external/my-nvim/nvim-config/README.md
@@ -9,7 +9,7 @@
- **状态**: 经过全面测试,无任何问题或报错
### 📦 包含内容
-- ✅ 最新版 Neovim v0.11.5 AppImage 可执行文件
+- ✅ LazyVim 配置文件;Neovim 可执行文件需从官方渠道或系统包管理器安装
- ✅ 标准 LazyVim 配置框架
- ✅ 修复的 Neotree 配置(无重复问题)
- ✅ 默认 tokyonight 主题
@@ -29,22 +29,18 @@ cd vim
# 复制配置文件
cp -r nvim-config/* ~/.config/
-# 复制可执行文件
-cp nvim-config/nvim ~/.local/bin/
-chmod +x ~/.local/bin/nvim
-
-# 确保路径在 PATH 中
-echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
-source ~/.bashrc
+# 安装 Neovim
+# 推荐使用系统包管理器、官方 AppImage 或 GitHub Release。
+# 不再在本仓库跟踪 nvim 二进制运行时。
```
### 3. 启动使用
```bash
# 直接启动
-~/.local/bin/nvim
+nvim
# 或使用别名(推荐)
-alias n='~/.local/bin/nvim'
+alias n='nvim'
n
```
@@ -118,10 +114,10 @@ vim.api.nvim_create_autocmd("VimEnter", {
### 启动测试
```bash
# 基础启动测试
-~/.local/bin/nvim --headless -c "echo 'OK'" -c "qa"
+nvim --headless -c "echo 'OK'" -c "qa"
# 配置加载测试
-~/.local/bin/nvim --headless -c "lua print('Config OK')" -c "qa"
+nvim --headless -c "lua print('Config OK')" -c "qa"
```
### 健康检查
@@ -135,7 +131,6 @@ nvim-config/
├── init.lua # 入口文件
├── lazy-lock.json # 插件锁文件
├── lazyvim.json # LazyVim 配置
-├── nvim # Neovim v0.11.5 AppImage
├── lua/
│ ├── config/
│ │ ├── autocmds.lua # 自动命令
@@ -197,4 +192,4 @@ Ctrl+w h/j/k/l
- ✅ 美观实用的界面设计
- ✅ 经过全面测试验证
-**确定没有任何问题和报错** - 你可以放心使用这份完美配置!
\ No newline at end of file
+**确定没有任何问题和报错** - 你可以放心使用这份完美配置!
diff --git a/assets/repo/my-nvim/nvim-config/init.lua b/tools/external/my-nvim/nvim-config/init.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/init.lua
rename to tools/external/my-nvim/nvim-config/init.lua
diff --git a/assets/repo/my-nvim/nvim-config/lazy-lock.json b/tools/external/my-nvim/nvim-config/lazy-lock.json
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lazy-lock.json
rename to tools/external/my-nvim/nvim-config/lazy-lock.json
diff --git a/assets/repo/my-nvim/nvim-config/lazyvim.json b/tools/external/my-nvim/nvim-config/lazyvim.json
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lazyvim.json
rename to tools/external/my-nvim/nvim-config/lazyvim.json
diff --git a/assets/repo/my-nvim/nvim-config/lua/config/autocmds.lua b/tools/external/my-nvim/nvim-config/lua/config/autocmds.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/config/autocmds.lua
rename to tools/external/my-nvim/nvim-config/lua/config/autocmds.lua
diff --git a/assets/repo/my-nvim/nvim-config/lua/config/keymaps.lua b/tools/external/my-nvim/nvim-config/lua/config/keymaps.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/config/keymaps.lua
rename to tools/external/my-nvim/nvim-config/lua/config/keymaps.lua
diff --git a/assets/repo/my-nvim/nvim-config/lua/config/lazy.lua b/tools/external/my-nvim/nvim-config/lua/config/lazy.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/config/lazy.lua
rename to tools/external/my-nvim/nvim-config/lua/config/lazy.lua
diff --git a/assets/repo/my-nvim/nvim-config/lua/config/options.lua b/tools/external/my-nvim/nvim-config/lua/config/options.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/config/options.lua
rename to tools/external/my-nvim/nvim-config/lua/config/options.lua
diff --git a/assets/repo/my-nvim/nvim-config/lua/plugins/colorscheme.lua b/tools/external/my-nvim/nvim-config/lua/plugins/colorscheme.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/plugins/colorscheme.lua
rename to tools/external/my-nvim/nvim-config/lua/plugins/colorscheme.lua
diff --git a/assets/repo/my-nvim/nvim-config/lua/plugins/dracula.lua b/tools/external/my-nvim/nvim-config/lua/plugins/dracula.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/plugins/dracula.lua
rename to tools/external/my-nvim/nvim-config/lua/plugins/dracula.lua
diff --git a/assets/repo/my-nvim/nvim-config/lua/plugins/example.lua b/tools/external/my-nvim/nvim-config/lua/plugins/example.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/plugins/example.lua
rename to tools/external/my-nvim/nvim-config/lua/plugins/example.lua
diff --git a/assets/repo/my-nvim/nvim-config/lua/plugins/snacks.lua b/tools/external/my-nvim/nvim-config/lua/plugins/snacks.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/plugins/snacks.lua
rename to tools/external/my-nvim/nvim-config/lua/plugins/snacks.lua
diff --git a/assets/repo/my-nvim/nvim-config/lua/plugins/ui.lua b/tools/external/my-nvim/nvim-config/lua/plugins/ui.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/plugins/ui.lua
rename to tools/external/my-nvim/nvim-config/lua/plugins/ui.lua
diff --git a/assets/repo/my-nvim/nvim-config/lua/themes/dracula.lua b/tools/external/my-nvim/nvim-config/lua/themes/dracula.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/lua/themes/dracula.lua
rename to tools/external/my-nvim/nvim-config/lua/themes/dracula.lua
diff --git a/assets/repo/my-nvim/nvim-config/stylua.toml b/tools/external/my-nvim/nvim-config/stylua.toml
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/stylua.toml
rename to tools/external/my-nvim/nvim-config/stylua.toml
diff --git a/assets/repo/my-nvim/nvim-config/theme-previewer.lua b/tools/external/my-nvim/nvim-config/theme-previewer.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/theme-previewer.lua
rename to tools/external/my-nvim/nvim-config/theme-previewer.lua
diff --git a/assets/repo/my-nvim/nvim-config/theme-switcher.lua b/tools/external/my-nvim/nvim-config/theme-switcher.lua
similarity index 100%
rename from assets/repo/my-nvim/nvim-config/theme-switcher.lua
rename to tools/external/my-nvim/nvim-config/theme-switcher.lua
diff --git a/assets/repo/tmux b/tools/external/tmux
similarity index 100%
rename from assets/repo/tmux
rename to tools/external/tmux
diff --git a/assets/repo/prompts-library/.gitignore b/tools/prompts-library/.gitignore
similarity index 100%
rename from assets/repo/prompts-library/.gitignore
rename to tools/prompts-library/.gitignore
diff --git a/assets/repo/prompts-library/README.md b/tools/prompts-library/README.md
similarity index 89%
rename from assets/repo/prompts-library/README.md
rename to tools/prompts-library/README.md
index 0a0bb3b..755898b 100644
--- a/assets/repo/prompts-library/README.md
+++ b/tools/prompts-library/README.md
@@ -25,7 +25,7 @@
@@ -171,6 +171,21 @@ python3 scripts/gemini_jsonl_batch.py --input 2 --output 2/prompts.jsonl --model
- 可用 `-v` 查看逐文件处理日志,`--gemini-cmd` 自定义 CLI 可执行路径。