feat: agent-agnostic refactor, smart SPA discovery, marketplace pipeline (#336)

* feat: fix unified scraper pipeline gaps, add multi-agent support, and Unity skill configs

Fix multiple bugs in the unified scraper pipeline discovered while creating Unity skills
(Spine, Addressables, DOTween):

- Fix doc scraper KeyError by passing base_url in temp config
- Fix scraped_data list-vs-dict bug in detect_conflicts() and merge_sources()
- Add Phase 6 auto-enhancement from config "enhancement" block (LOCAL + API mode)
- Add "browser": true config support for JavaScript SPA documentation sites
- Add Phase 3 skip message for better UX
- Add subprocess timeout (3600s) for doc scraper
- Fix SkillEnhancer missing skill_dir argument in API mode
- Fix browser renderer defaults (60s timeout, domcontentloaded wait condition)
- Fix C3.x JSON filename mismatch (design_patterns.json → all_patterns.json)
- Fix workflow builtin target handling when no pattern data available
- Make AI enhancement timeout configurable via SKILL_SEEKER_ENHANCE_TIMEOUT env var (300s default)
- Add C#, Go, Rust, Swift, Ruby, PHP, GDScript to GitHub scraper extension map
- Add multi-agent LOCAL mode support across all 17 scrapers (--agent flag)
- Add Kimi/Moonshot platform support (API keys, agent presets, config wizard)
- Add unity-game-dev.yaml workflow (7 stages covering Unity-specific patterns)
- Add 3 Unity skill configs (Spine, Addressables, DOTween)
- Add comprehensive Claude bias audit report

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: create AgentClient abstraction, remove hardcoded Claude from 5 enhancers (#334)

Phase 1 of the full agent-agnostic refactor. Creates a centralized AgentClient
that all enhancers use instead of hardcoded subprocess calls and model names.

New file:
- agent_client.py: Unified AI client supporting API mode (Anthropic, Moonshot,
  Google, OpenAI) and LOCAL mode (Claude Code, Kimi, Codex, Copilot, OpenCode,
  custom agents). Provides detect_api_key(), get_model(), detect_default_target().

Refactored (removed all hardcoded ["claude", ...] subprocess calls):
- ai_enhancer.py: -140 lines, delegates to AgentClient
- config_enhancer.py: -150 lines, removed _run_claude_cli()
- guide_enhancer.py: -120 lines, removed _check_claude_cli(), _call_claude_*()
- unified_enhancer.py: -100 lines, removed _check_claude_cli(), _call_claude_*()
- codebase_scraper.py: collapsed 3 functions into 1 using AgentClient

Fixed:
- utils.py: has_api_key()/get_api_key() now check all providers
- enhance_skill.py, video_scraper.py, video_visual.py: model names configurable
  via ANTHROPIC_MODEL env var
- enhancement_workflow.py: uses call() with _call_claude() fallback

Net: -153 lines of code while adding full multi-agent support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: Phase 2 agent-agnostic refactor — defaults, help text, merge mode, MCP (#334)

Phase 2 of the full agent-agnostic refactor:

Default targets:
- Changed default="claude" to auto-detect from API keys in 5 argument files
  and 3 CLI scripts (install_skill, upload_skill, enhance_skill)
- Added AgentClient.detect_default_target() for runtime resolution
- MCP server functions now use "auto" default with runtime detection

Help text (16+ argument files):
- Replaced "ANTHROPIC_API_KEY" / "Claude Code" with agent-neutral wording
- Now mentions all API keys (ANTHROPIC, MOONSHOT, etc.) and "AI coding agent"

Log messages:
- main.py, enhance_command.py: "Claude Code CLI" → dynamic agent name
- enhance_command.py docstring: "Claude Code" → "AI coding agent"

Merge mode rename:
- Added "ai-enhanced" as the preferred merge mode name
- "claude-enhanced" kept as backward-compatible alias
- Renamed ClaudeEnhancedMerger → AIEnhancedMerger (with alias)
- Updated choices, validators, and descriptions

MCP server descriptions:
- server_fastmcp.py: "Claude AI skills" → "LLM skills" in tool descriptions
- packaging_tools.py: Updated defaults and dry-run messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: Phase 3 agent-agnostic refactor — docstrings, MCP descriptions, README (#334)

Phase 3 of the full agent-agnostic refactor:

Module docstrings (17+ scraper files):
- "Claude Skill Converter" → "AI Skill Converter"
- "Build Claude skill" → "Build AI/LLM skill"
- "Asking Claude" → "Asking AI"
- Updated doc_scraper, github_scraper, pdf_scraper, word_scraper,
  epub_scraper, video_scraper, enhance_skill, enhance_skill_local,
  unified_scraper, and others

MCP server_legacy.py (30+ fixes):
- All tool descriptions: "Claude skill" → "LLM skill"
- "Upload to Claude" → "Upload skill"
- "enhance with Claude Code" → "enhance with AI agent"
- Kept claude.ai/skills URLs (platform-specific, correct)

MCP README.md:
- Added multi-agent support note at top
- "Claude AI skills" → "LLM skills" throughout
- Updated examples to show multi-platform usage
- Kept Claude Code in supported agents list (accurate)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: Phase 3 continued — remaining docstring and comment fixes (#334)

Additional agent-neutral text fixes in 8 files missed from the initial
Phase 3 commit:
- config_extractor.py, config_manager.py, constants.py: comments
- enhance_command.py: docstring and print messages
- guide_enhancer.py: class/module docstrings
- parsers/enhance_parser.py, install_parser.py: help text
- signal_flow_analyzer.py: docstring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* workflow added

* fix: address code review issues in AgentClient and Phase 6 (#334)

Fixes found during commit review:

1. AgentClient._call_local: Only append "Write your response to:" when
   caller explicitly passes output_file (was always appending)

2. Codex agent: Added uses_stdin flag to preset, pipe prompt via stdin
   instead of DEVNULL (codex reads from stdin with "-" arg)

3. Provider detection: Added _detect_provider_from_key() to detect
   provider from API key prefix (sk-ant- → anthropic, AIza → google)
   instead of always assuming anthropic

4. Phase 6 API mode: Replaced direct SkillEnhancer/ANTHROPIC_API_KEY
   with AgentClient for multi-provider support (Moonshot, Google, OpenAI)

5. config_enhancer: Removed output_file path from prompt — AgentClient
   manages temp files and output detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: make claude adaptor model name configurable via ANTHROPIC_MODEL env var

Missed in the Phase 1 refactor — adaptors/claude.py:381 had a hardcoded
model name without the os.environ.get() wrapper that all other files use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add copilot stdin support, custom agent, and kimi aliases (#334)

Additional agent improvements from Kimi review:
- Added uses_stdin: True to copilot agent preset (reads from stdin like codex)
- Added custom agent support via SKILL_SEEKER_AGENT_CMD env var in _call_local()
- Added kimi_code/kimi-code aliases in normalize_agent_name()
- Added "kimi" to --target choices in enhance arguments
- Updated help text with MOONSHOT_API_KEY across argument files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Kimi CLI integration — add uses_stdin and output parsing (#334)

Kimi CLI's --print mode requires stdin piping and outputs structured
protocol messages (TurnBegin, TextPart, etc.) instead of plain text.

Fixes:
- Added uses_stdin: True to kimi preset (was not piping prompt)
- Added parse_output: "kimi" flag to preset
- Added _parse_kimi_output() to extract text from TextPart lines
- Kimi now returns clean text instead of raw protocol dump

Tested: kimi returns '{"status": "ok"}' correctly via AgentClient.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Kimi CLI in enhance_skill_local — remove wrong skip-permissions, use absolute path

Two bugs in enhance_skill_local.py AGENT_PRESETS for Kimi:
1. supports_skip_permissions was True — Kimi doesn't support
   --dangerously-skip-permissions, only Claude does. Fixed to False.
2. {skill_dir} was resolved as relative path — Kimi CLI requires
   absolute paths for --work-dir. Fixed with .resolve().

Tested: `skill-seekers enhance output/test-e2e/ --agent kimi`
now works end-to-end (107s, 9233 bytes output).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove invalid --enhance-level flag from enhance subprocess calls

doc_scraper.py and video_scraper.py were passing --enhance-level to
skill-seekers-enhance, which doesn't accept that flag. This caused
enhancement to fail silently after scraping completed.

Fixes:
- Removed --enhance-level from enhance subprocess calls
- Added --agent passthrough in doc_scraper.py
- Fixed log messages to show correct command

Tested: `skill-seekers create <url> --enhance-level 1` now chains
scrape → enhance successfully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add --agent and --agent-cmd to create command UNIVERSAL_ARGUMENTS

The --agent flag was defined in common.py but not imported into the
create command's UNIVERSAL_ARGUMENTS, so it wasn't available when using
`skill-seekers create <source> --agent kimi`. Now all 17 source types
support the --agent flag via the create command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update docs data_file path after moving to cache directory

The scraped_data["documentation"] stored the original output/ path for
data_file, but the directory was moved to .skillseeker-cache/ afterward.
Phase 2 conflict detection then failed with FileNotFoundError trying to
read the old path.

Now updates data_file to point to the cache location after the move.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: multi-language code signature extraction in GitHub scraper

The GitHub scraper only analyzed files matching the primary language
(by bytes). For multi-language repos like spine-runtimes (C++ primary
but C# is the target), this meant 0 C# files were analyzed.

Fix: Analyze top 3 languages with known extension mappings instead of
just the primary. Also support "language" field in config source to
explicitly target specific languages (e.g., "language": "C#").

Updated Unity configs to specify language: "C#" for focused analysis.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: per-file language detection + remove artificial analysis limits

Rewrites GitHub scraper's _extract_signatures_and_tests() to detect
language per-file from extension instead of only analyzing the primary
language. This fixes multi-language repos like spine-runtimes (C++ primary)
where C# files were never analyzed.

Changes:
- Build reverse ext→language map, detect language per-file
- Analyze ALL files with known extensions (not just primary language)
- Config "language" field works as optional filter, not a workaround
- Store per-file language + languages_analyzed in output
- Remove 50-file API mode limit (rate limiting already handles this)
- Remove 100-file default config extraction limit (now unlimited by default)
- Fix unified scraper default max_pages from 100 to 500 (matches constants.py)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove remaining 100-file limit in config_extractor.extract_from_directory

The find_config_files default was changed to unlimited but
extract_from_directory and CLI --max-files still defaulted to 100.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: replace interactive terminal merge with automated AgentClient call

AIEnhancedMerger._launch_claude_merge() used to open a terminal window,
run a bash script, and poll for a file — requiring manual interaction.

Now uses AgentClient.call() to send the merge prompt directly and parse
the JSON response. Fully automated, no terminal needed, works with any
configured AI agent (Claude, Kimi, etc.).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add marketplace pipeline for publishing skills to Claude Code plugin repos

Connect the three-repo pipeline: configs repo → Skill Seekers engine → plugin
marketplace repos. Enables automated publishing of generated skills directly
into Claude Code plugin repositories with proper plugin.json and marketplace.json
structure.

New components:
- MarketplaceManager: Registry for plugin marketplace repos at
  ~/.skill-seekers/marketplaces.json with per-repo git tokens, branch config,
  and default author metadata
- MarketplacePublisher: Clones marketplace repo, creates plugin directory
  structure (skills/, .claude-plugin/plugin.json), updates marketplace.json,
  commits and pushes. Includes skill_name validation to prevent path traversal,
  and cleanup of partial state on git failures
- 4 MCP tools: add_marketplace, list_marketplaces, remove_marketplace,
  publish_to_marketplace — registered in FastMCP server
- Phase 6 in install workflow: automatic marketplace publishing after packaging,
  triggered by --marketplace CLI arg or marketplace_targets config field

CLI additions:
- --marketplace NAME: publish to registered marketplace after packaging
- --marketplace-category CAT: plugin category (default: development)
- --create-branch: create feature branch instead of committing to main

Security:
- Skill name regex validation (^[a-zA-Z0-9][a-zA-Z0-9._-]*$) prevents path
  traversal attacks via malicious SKILL.md frontmatter
- has_api_key variable scoping fix in install workflow summary
- try/finally cleanup of partial plugin directories on publish failure

Config schema:
- Optional marketplace_targets field in config JSON for multi-marketplace
  auto-publishing: [{"marketplace": "spyke", "category": "development"}]
- Backward compatible — ignored by older versions

Tests: 58 tests (36 manager + 22 publisher including 2 integration tests
using file:// git protocol for full publish success path)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: thread agent selection through entire enhancement pipeline

Propagates the --agent and --agent-cmd CLI parameters through all
enhancement components so users can use any supported coding agent
(kimi, claude, copilot, codex, opencode) consistently across the
full pipeline, not just in top-level enhancement.

Agent parameter threading:
- AIEnhancer: accepts agent param, passes to AgentClient
- ConfigEnhancer: accepts agent param, passes to AgentClient
- WorkflowEngine: accepts agent param, passes to sub-enhancers
  (PatternEnhancer, TestExampleEnhancer, AIEnhancer)
- ArchitecturalPatternDetector: accepts agent param for AI enhancement
- analyze_codebase(): accepts agent/agent_cmd, forwards to
  ConfigEnhancer, ArchitecturalPatternDetector, and doc processing
- UnifiedScraper: reads agent from CLI args, forwards to doc scraper
  subprocess, C3.x analysis, and LOCAL enhancement
- CreateCommand: forwards --agent and --agent-cmd to subprocess argv
- workflow_runner: passes agent to WorkflowEngine for inline/named workflows

Timeout improvements:
- Default enhancement timeout increased from 300s (5min) to 2700s (45min)
  to accommodate large skill generation with local agents
- New get_default_timeout() in agent_client.py with env var override
  (SKILL_SEEKER_ENHANCE_TIMEOUT) supporting 'unlimited' value
- Config enhancement block supports "timeout": "unlimited" field
- Removed hardcoded timeout=300 and timeout=600 calls in config_enhancer
  and merge_sources, now using centralized default

CLI additions (unified_scraper):
- --agent AGENT: select local coding agent for enhancement
- --agent-cmd CMD: override agent command template (advanced)

Config: unity-dotween.json updated with agent=kimi, timeout=unlimited,
removed unused file_patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add claude-code unified config for Claude Code CLI skill generation

Unified config combining official Claude Code documentation and source
code analysis. Covers internals, architecture, tools, commands, IDE
integrations, MCP, plugins, skills, and development workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add multi-agent support verification report and test artifacts

- AGENT_SUPPORT_VERIFICATION.md: verification report confirming agent
  parameter threading works across all enhancement components
- END_TO_END_EXAMPLES.md: complete workflows for all 17 source types
  with both Claude and Kimi agents
- test_agents.sh: shell script for real-world testing of agent support
  across major CLI commands with both agents
- test_realworld.md: real-world test scenarios for manual QA

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add .env to .gitignore to prevent secret exposure

The .env file containing API keys (ANTHROPIC_API_KEY, GITHUB_TOKEN, etc.)
was not in .gitignore, causing it to appear as untracked and risking
accidental commit. Added .env, .env.local, and .env.*.local patterns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: URL filtering uses base directory instead of full page URL (#331)

is_valid_url() checked url.startswith(self.base_url) where base_url
could be a full page path like ".../manual/index.html". Sibling pages
like ".../manual/LoadingAssets.html" failed the check because they
don't start with ".../index.html".

Now strips the filename to get the directory prefix:
  "https://example.com/docs/index.html" → "https://example.com/docs/"

This fixes SPA sites like Unity's DocFX docs where browser mode renders
the page but sibling links were filtered out.

Closes #331

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pass language config through to GitHub scraper in unified flow

The unified scraper built github_config from source fields but didn't
include the "language" field. The GitHub scraper's per-file detection
read self.config.get("language", "") which was always empty, so it
fell back to analyzing all languages instead of the focused C# filter.

For DOTween (C# only repo), this caused 0 files analyzed because
without the language filter, it analyzed top 3 languages but the
file tree matching failed silently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: centralize all enhancement timeouts to 45min default with unlimited support

All enhancement/AI timeouts now use get_default_timeout() from
agent_client.py instead of scattered hardcoded values (120s, 300s, 600s).

Default: 2700s (45 minutes)
Override: SKILL_SEEKER_ENHANCE_TIMEOUT env var
Unlimited: Set to "unlimited", "none", or "0"

Updated: agent_client.py, enhance_skill_local.py, arguments/enhance.py,
enhance_command.py, unified_enhancer.py, unified_scraper.py

Not changed (different purposes):
- Browser page load timeout (60s)
- API HTTP request timeout (120s)
- Doc scraper subprocess timeout (3600s)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add browser_wait_until and browser_extra_wait config for SPA docs

DocFX sites (Unity docs) render navigation via JavaScript after initial
page load. With domcontentloaded, only 1 link was found. With
networkidle + 5s extra wait, 95 content pages are discovered.

New config options for documentation sources:
- browser_wait_until: "networkidle" | "load" | "domcontentloaded"
- browser_extra_wait: milliseconds to wait after page load for lazy nav

Updated Addressables config to use networkidle + 5000ms extra wait.
Pass browser settings through unified scraper to doc scraper config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: three-layer smart discovery engine for SPA documentation sites

Replaces the browser_wait_until/browser_extra_wait config hacks with
a proper discovery engine that runs before the BFS crawl loop:

Layer 1: sitemap.xml — checks domain root for sitemap, parses <loc> tags
Layer 2: llms.txt — existing mechanism (unchanged)
Layer 3: SPA nav — renders index page with networkidle via Playwright,
         extracts all links from the fully-rendered DOM sidebar/TOC

The BFS crawl then uses domcontentloaded (fast) since all pages are
already discovered. No config hacks needed — browser mode automatically
triggers SPA discovery when only 1 page is found.

Tested: Unity Addressables DocFX site now discovers 95 pages (was 1).
Removed browser_wait_until/browser_extra_wait from Addressables config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: replace manual arg forwarding with dynamic routing in create command

The create command manually hardcoded ~60% of scraper flags in _route_*()
methods, causing ~40 flags to be silently dropped. Every new flag required
edits in 2 places (arguments/create.py + create_command.py), guaranteed
to drift.

Replaced with _build_argv() — a dynamic forwarder that iterates
vars(self.args) and forwards all explicitly-set arguments automatically,
using the same pattern as main.py::_reconstruct_argv(). This eliminates
the root cause of all flag gaps.

Changes in create_command.py (-380 lines, +175 lines = net -205):
- Added _build_argv() dynamic arg forwarder with dest→flag translation
  map for mismatched names (async_mode→--async, video_playlist→--playlist,
  skip_config→--skip-config-patterns, workflow_var→--var)
- Added _call_module() helper (dedup sys.argv swap pattern)
- Simplified all _route_*() methods from 50-70 lines to 5-10 lines each
- Deleted _add_common_args() entirely (subsumed by _build_argv)
- _route_generic() now forwards ALL args, not just universal ones

New flags accessible via create command:
- --from-json: build skill from pre-extracted JSON (all source types)
- --skip-api-reference: skip API reference generation (local codebase)
- --skip-dependency-graph: skip dependency analysis (local codebase)
- --skip-config-patterns: skip config pattern extraction (local codebase)
- --no-comments: skip comment extraction (local codebase)
- --depth: analysis depth control (local codebase, deprecated)
- --setup: auto-detect GPU/install video deps (video)

Bug fix in unified_scraper.py:
- Fixed C3.x pattern data loss: unified_scraper read patterns/detected_patterns.json
  but codebase_scraper writes patterns/all_patterns.json. Changed both read
  locations (line 828 for local sources, line 1597 for GitHub C3.x) to use
  the correct filename. This was causing 100% loss of design pattern data
  (e.g., 905 patterns detected but 0 included in final skill).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address 5 code review issues in marketplace and package pipeline

Fixes found by automated code review of the marketplace feature and
package command:

1. --marketplace flag silently ignored in package_skill.py CLI
   Added MarketplacePublisher invocation after successful packaging
   when --marketplace is provided. Previously the flag was parsed
   but never acted on.

2. Missing 7 platform choices in --target (package.py)
   Added minimax, opencode, deepseek, qwen, openrouter, together,
   fireworks to the argparse choices list. These platforms have
   registered adaptors but were rejected by the argument parser.

3. is_update always True for new marketplace registrations
   Two separate datetime.now() calls produced different microsecond
   timestamps, making added_at != updated_at always. Fixed by
   assigning a single timestamp to both fields.

4. Shallow clone (depth=1) caused push failures for marketplace repos
   MarketplacePublisher now does full clones instead of using
   GitConfigRepo's shallow clone (which is designed for read-only
   config fetching). Full clone is required for commit+push workflow.

5. Partial plugin dir not cleaned on force=True failure
   Removed the `and not force` guard from cleanup logic — if an
   operation fails midway, the partial directory should be cleaned
   regardless of whether force was set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address dynamic routing edge cases in create_command

Fixes from code review of the _build_argv() refactor:

1. Non-None defaults forwarded unconditionally — added enhance_level=2,
   doc_version="", video_languages="en", whisper_model="base",
   platform="slack", visual_interval=0.7, visual_min_gap=0.5,
   visual_similarity=3.0 to the defaults dict so they're only forwarded
   when the user explicitly overrides them. This fixes video sources
   incorrectly getting --enhance-level 2 (video default is 0).

2. video_url dest not translated — added "video_url": "--url" to
   _DEST_TO_FLAG so create correctly forwards --video-url as --url
   to video_scraper.py.

3. Video positional args double-forwarded — added video_url,
   video_playlist, video_file to _SKIP_ARGS since _route_video()
   already handles them via positional args from source detection.

4. Removed dead workflow_var entry from _DEST_TO_FLAG — the create
   parser uses key "var" not "workflow_var", so the translation
   was never triggered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve 15 broken tests and --from-json crash bug in create command

Fixes found by Kimi code review of the dynamic routing refactor:

1. 3 test_create_arguments.py failures — UNIVERSAL_ARGUMENTS count
   changed from 19 to 21 (added agent, agent_cmd). Updated expected
   count and name set. Moved from_json out of UNIVERSAL to
   ADVANCED_ARGUMENTS since not all scrapers support it.

2. 12 test_create_integration_basic.py failures — tests called
   _add_common_args() which was deleted in the refactor. Rewrote
   _collect_argv() to use _build_argv() via CreateCommand with
   SourceDetector. Updated _make_args defaults to match new
   parameter set.

3. --from-json crash bug — was in UNIVERSAL_ARGUMENTS so create
   accepted it for all source types, but web/github/local scrapers
   don't support it. Forwarding it caused argparse "unrecognized
   arguments" errors. Moved to ADVANCED_ARGUMENTS with documentation
   listing which source types support it.

4. Additional _is_explicitly_set defaults — added enhance_level=2,
   doc_version="", video_languages="en", whisper_model="base",
   platform="slack", visual_interval/min_gap/similarity defaults to
   prevent unconditional forwarding of parser defaults.

5. Video arg handling — added video_url to _DEST_TO_FLAG translation
   map, added video_url/video_playlist/video_file to _SKIP_ARGS
   (handled as positionals by _route_video).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: C3.x analysis data loss — read from references/ after _generate_references() cleanup

Root cause: _generate_references() in codebase_scraper.py copies analysis
directories (patterns/, test_examples/, config_patterns/, architecture/,
dependencies/, api_reference/) into references/ then DELETES the originals
to avoid duplication (Issue #279). But unified_scraper.py reads from the
original paths after analyze_codebase() returns — by which time the
originals are gone.

This caused 100% data loss for all 6 C3.x data types (design patterns,
test examples, config patterns, architecture, dependencies, API reference)
in the unified scraper pipeline. The data was correctly detected (e.g.,
905 patterns in 510 files) but never made it into the final skill.

Fix: Added _load_json_fallback() method that checks references/{subdir}/
first (where _generate_references moves the data), falling back to the
original path. Applied to both GitHub C3.x analysis (line ~1599) and
local source analysis (line ~828).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add allowlist to _build_argv for config route to unified_scraper

_build_argv() was forwarding all CLI args (--name, --doc-version, etc.)
to unified_scraper which doesn't accept them. Added allowlist parameter
to _build_argv() — when provided, ONLY args in the allowlist are forwarded.

The config route now uses _UNIFIED_SCRAPER_ARGS allowlist with the exact
set of flags unified_scraper accepts.

This is a targeted patch — the proper fix is the ExecutionContext singleton
refactor planned separately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add force=True to marketplace publish from package CLI

The package command's --marketplace flag didn't pass force=True to
MarketplacePublisher, so re-publishing an existing skill would fail
with "already exists" error instead of overwriting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add push_config tool for publishing configs to registered source repos

New ConfigPublisher class that validates configs, places them in the
correct category directory, commits, and pushes to registered source
repositories. Follows the MarketplacePublisher pattern.

Features:
- Auto-detect category from config name/description
- Validate via ConfigValidator + repo's validate-config.py
- Support feature branch or direct push
- Force overwrite existing configs
- MCP tool: push_config(config_path, source_name, category)

Usage:
  push_config(config_path="configs/unity-spine.json", source_name="spyke")

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: security hardening, error handling, tests, and cleanup

Security:
- Remove command injection via cloned repo script execution (config_publisher)
- Replace git add -A with targeted staging (marketplace_publisher)
- Clear auth tokens from cached .git/config after clone
- Use defusedxml for sitemap XML parsing (XXE protection)
- Add path traversal validation for config names

Error handling:
- AgentClient: specific exception handling for rate limit, auth, connection errors
- AgentClient: log subprocess stderr on non-zero exit, raise on explicit API mode failure
- config_publisher: only catch ValueError for validation warnings

Logic bugs:
- Fix _build_argv silently dropping --enhance-level 2 (matched default)
- Fix URL filtering over-broadening (strip to parent instead of adding /)
- Log warning when _call_module returns None exit code

Tests (134 new):
- test_agent_client.py: 71 tests for normalize, detect, init, timeout, model
- test_config_publisher.py: 23 tests for detect_category, publish, errors
- test_create_integration_basic.py: 20 tests for _build_argv routing
- Fix 11 pre-existing failures (guide_enhancer, doctor, install_skill, marketplace)

Cleanup:
- Remove 5 dev artifact files (-1405 lines)
- Rename _launch_claude_merge to _launch_ai_merge

All 3194 tests pass, 39 expected skips.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pin ruff==0.15.8 in CI and reformat packaging_tools.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing pytest install to vector DB adaptor test jobs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reformat 7 files for ruff 0.15.8 and fix vector DB test path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove test-week2-integration job referencing missing script

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update e2e test to accept dynamic platform name in upload phase

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: YusufKaraaslanSpyke <yusuf@spykegames.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yusyus
2026-04-02 04:50:15 +03:00
committed by GitHub
parent c29fad606c
commit c6a6db01bf
104 changed files with 6251 additions and 1740 deletions

View File

@@ -1,7 +1,7 @@
"""Skill Seekers CLI tools package.
This package provides command-line tools for converting documentation
websites into Claude AI skills.
websites into AI skills.
Main modules:
- doc_scraper: Main documentation scraping and skill building tool
@@ -13,7 +13,7 @@ Main modules:
- 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
- upload_skill: Upload skills to target platform
- utils: Shared utility functions
"""

View File

@@ -378,7 +378,7 @@ version: {metadata.version}
client = anthropic.Anthropic(**client_kwargs)
message = client.messages.create(
model="claude-sonnet-4-20250514",
model=os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514"),
max_tokens=4096,
temperature=0.3,
messages=[{"role": "user", "content": prompt}],

View File

@@ -0,0 +1,553 @@
"""
Unified AI Client for Skill Seekers
Centralizes all AI invocations (API and LOCAL mode) so that every enhancer
uses a single abstraction instead of hardcoding subprocess calls or model names.
Supports:
- API mode: Anthropic, Moonshot/Kimi, Google Gemini, OpenAI (via adaptor pattern)
- LOCAL mode: Claude Code, Kimi Code, Codex, Copilot, OpenCode, custom agents
Usage:
from skill_seekers.cli.agent_client import AgentClient
client = AgentClient(mode="auto")
response = client.call("Analyze this code and return JSON")
# Or with explicit agent
client = AgentClient(mode="local", agent="kimi")
response = client.call(prompt, timeout=300)
# Static helpers
key, provider = AgentClient.detect_api_key()
model = AgentClient.get_model()
"""
import logging
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
logger = logging.getLogger(__name__)
# Agent presets for LOCAL mode — reused from enhance_skill_local.py pattern
AGENT_PRESETS = {
"claude": {
"display_name": "Claude Code",
"command": ["claude", "--dangerously-skip-permissions", "{prompt_file}"],
"version_check": ["claude", "--version"],
},
"codex": {
"display_name": "OpenAI Codex CLI",
"command": ["codex", "exec", "--full-auto", "--skip-git-repo-check", "-"],
"version_check": ["codex", "--version"],
"uses_stdin": True,
},
"copilot": {
"display_name": "GitHub Copilot CLI",
"command": ["gh", "copilot", "chat", "-"],
"version_check": ["gh", "copilot", "--version"],
"uses_stdin": True,
},
"opencode": {
"display_name": "OpenCode CLI",
"command": ["opencode"],
"version_check": ["opencode", "--version"],
},
"kimi": {
"display_name": "Kimi Code CLI",
"command": [
"kimi",
"--print",
"--input-format",
"text",
"--work-dir",
"{cwd}",
],
"version_check": ["kimi", "--version"],
"uses_stdin": True,
"parse_output": "kimi", # Needs special output parsing
},
}
# Default models per API provider
DEFAULT_MODELS = {
"anthropic": "claude-sonnet-4-20250514",
"moonshot": "moonshot-v1-auto",
"google": "gemini-2.0-flash",
"openai": "gpt-4o",
}
# API key env var → provider mapping
API_KEY_MAP = {
"ANTHROPIC_API_KEY": "anthropic",
"ANTHROPIC_AUTH_TOKEN": "anthropic",
"MOONSHOT_API_KEY": "moonshot",
"GOOGLE_API_KEY": "google",
"OPENAI_API_KEY": "openai",
}
DEFAULT_ENHANCE_TIMEOUT = 2700 # 45 minutes
UNLIMITED_TIMEOUT = 86400 # 24 hours (subprocess requires a finite number)
def get_default_timeout() -> int:
"""Return default enhancement timeout in seconds.
Priority:
1. SKILL_SEEKER_ENHANCE_TIMEOUT environment variable
2. DEFAULT_ENHANCE_TIMEOUT (45 minutes)
Supports 'unlimited' or 0/negative values which map to UNLIMITED_TIMEOUT (24h).
"""
env_val = os.environ.get("SKILL_SEEKER_ENHANCE_TIMEOUT", "").strip().lower()
if env_val in ("unlimited", "none", "0"):
return UNLIMITED_TIMEOUT
try:
timeout = int(env_val)
if timeout <= 0:
return UNLIMITED_TIMEOUT
return timeout
except ValueError:
return DEFAULT_ENHANCE_TIMEOUT
# Provider → target platform mapping (for --target defaults)
PROVIDER_TARGET_MAP = {
"anthropic": "claude",
"moonshot": "kimi",
"google": "gemini",
"openai": "openai",
}
def normalize_agent_name(agent_name: str) -> str:
"""Normalize agent name to canonical form."""
if not agent_name:
return "claude"
normalized = agent_name.strip().lower()
aliases = {
"claude-code": "claude",
"claude_code": "claude",
"codex-cli": "codex",
"copilot-cli": "copilot",
"open-code": "opencode",
"open_code": "opencode",
"kimi-cli": "kimi",
"kimi_code": "kimi",
"kimi-code": "kimi",
}
return aliases.get(normalized, normalized)
class AgentClient:
"""
Unified AI client that routes to API or LOCAL agent based on configuration.
All enhancers should use this instead of direct subprocess calls or API imports.
"""
def __init__(
self,
mode: str = "auto",
agent: str | None = None,
api_key: str | None = None,
):
"""
Initialize the agent client.
Args:
mode: "auto" (detect from env), "api" (force API), "local" (force CLI agent)
agent: LOCAL mode agent name ("claude", "kimi", "codex", "copilot", "opencode", "custom")
Resolved from: arg → env SKILL_SEEKER_AGENT → "claude"
api_key: API key override. If None, auto-detected from env vars.
"""
# Resolve agent name
env_agent = os.environ.get("SKILL_SEEKER_AGENT", "").strip()
self.agent = normalize_agent_name(agent or env_agent or "claude")
self.agent_display = AGENT_PRESETS.get(self.agent, {}).get("display_name", self.agent)
# Detect API key and provider
if api_key:
self.api_key = api_key
# Detect provider from key prefix or env vars
self.provider = self._detect_provider_from_key(api_key)
else:
self.api_key, self.provider = self.detect_api_key()
# Determine mode (keep original for error handling decisions)
self._requested_mode = mode
self.mode = mode
if mode == "auto":
if self.api_key:
self.mode = "api"
else:
self.mode = "local"
# Initialize API client if needed
self.client = None
if self.mode == "api" and self.api_key:
self.client = self._init_api_client()
@staticmethod
def _detect_provider_from_key(api_key: str) -> str:
"""Detect provider from API key prefix or fall back to env var check."""
if api_key.startswith("sk-ant-"):
return "anthropic"
if api_key.startswith("sk-"):
# Could be OpenAI or Moonshot — check env vars
if os.environ.get("MOONSHOT_API_KEY", "").strip() == api_key:
return "moonshot"
return "openai"
if api_key.startswith("AIza"):
return "google"
# Default: check which env var matches
for env_var, provider in API_KEY_MAP.items():
if os.environ.get(env_var, "").strip() == api_key:
return provider
return "anthropic" # Safe fallback
def _init_api_client(self):
"""Initialize the API client based on detected provider."""
try:
if self.provider == "anthropic":
import anthropic
kwargs = {"api_key": self.api_key}
base_url = os.environ.get("ANTHROPIC_BASE_URL")
if base_url:
kwargs["base_url"] = base_url
return anthropic.Anthropic(**kwargs)
elif self.provider == "moonshot":
import anthropic
return anthropic.Anthropic(
api_key=self.api_key,
base_url="https://api.moonshot.cn/v1",
)
elif self.provider == "openai":
from openai import OpenAI
return OpenAI(api_key=self.api_key)
elif self.provider == "google":
import google.generativeai as genai
genai.configure(api_key=self.api_key)
return genai
except ImportError as e:
logger.info(f"{self.provider} SDK not installed, falling back to LOCAL mode: {e}")
self.mode = "local"
except Exception as e:
if self._requested_mode == "api":
raise RuntimeError(f"Failed to initialize {self.provider} API client: {e}") from e
logger.error(f"Failed to initialize {self.provider} API client: {e}")
self.mode = "local"
return None
def call(
self,
prompt: str,
max_tokens: int = 4096,
timeout: int | None = None,
output_file: str | Path | None = None,
cwd: str | Path | None = None,
) -> str | None:
"""
Call the AI agent (API or LOCAL mode).
Args:
prompt: The prompt to send
max_tokens: Max response tokens (API mode only)
timeout: Timeout in seconds (default from SKILL_SEEKER_ENHANCE_TIMEOUT or 2700 = 45m)
output_file: Path for agent to write output (LOCAL mode, some agents)
cwd: Working directory for LOCAL mode subprocess
Returns:
Response text, or None on failure
"""
if timeout is None:
timeout = get_default_timeout()
if self.mode == "api":
return self._call_api(prompt, max_tokens)
elif self.mode == "local":
return self._call_local(prompt, timeout, output_file, cwd)
return None
def _call_api(self, prompt: str, max_tokens: int = 4096) -> str | None:
"""Call via API using the detected provider."""
if not self.client:
return None
model = self.get_model(self.provider)
try:
if self.provider in ("anthropic", "moonshot"):
response = self.client.messages.create(
model=model,
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
timeout=120,
)
return response.content[0].text
elif self.provider == "openai":
response = self.client.chat.completions.create(
model=model,
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
timeout=120,
)
return response.choices[0].message.content
elif self.provider == "google":
gmodel = self.client.GenerativeModel(model)
response = gmodel.generate_content(prompt)
return response.text
except Exception as e:
error_type = type(e).__name__
error_module = type(e).__module__ or ""
# Rate limit errors
if "rate" in error_type.lower() or "ratelimit" in error_type.lower():
logger.error(
f"{self.provider} API rate limited: {e}. "
"Retry after waiting or reduce request frequency."
)
return None
# Auth / permission errors
if "auth" in error_type.lower() or "permission" in error_type.lower():
logger.error(
f"{self.provider} API authentication failed: {e}. "
"Check your API key is valid and has sufficient permissions."
)
return None
# Timeout / connection errors
if (
any(
kw in error_type.lower()
for kw in ("timeout", "connect", "connection", "network")
)
or "httpx" in error_module.lower()
):
logger.error(
f"{self.provider} API connection error: {e}. "
"Check your network connectivity and try again."
)
return None
# All other errors
logger.error(f"{self.provider} API call failed: {e}")
return None
def _call_local(
self,
prompt: str,
timeout: int | None = None,
output_file: str | Path | None = None,
cwd: str | Path | None = None,
) -> str | None:
"""Call via LOCAL CLI agent using agent presets."""
if timeout is None:
timeout = get_default_timeout()
# Handle custom agent from env var
if self.agent == "custom":
custom_cmd = os.environ.get("SKILL_SEEKER_AGENT_CMD", "").strip()
if not custom_cmd:
logger.warning("⚠️ Custom agent selected but SKILL_SEEKER_AGENT_CMD not set")
return None
preset = {
"display_name": "Custom Agent",
"command": custom_cmd.split(),
"version_check": custom_cmd.split()[:1] + ["--version"],
}
else:
preset = AGENT_PRESETS.get(self.agent)
if not preset:
logger.warning(f"⚠️ Unknown agent: {self.agent}")
return None
try:
with tempfile.TemporaryDirectory(prefix="agent_client_") as temp_dir:
temp_path = Path(temp_dir)
prompt_file = temp_path / "prompt.md"
resp_file = Path(output_file) if output_file else (temp_path / "response.json")
# Only append output file instruction when caller explicitly requests it
full_prompt = prompt
if output_file:
full_prompt += f"\n\nWrite your response to: {resp_file}\n"
prompt_file.write_text(full_prompt)
# Build command from preset
cmd = []
for part in preset["command"]:
part = part.replace("{prompt_file}", str(prompt_file))
part = part.replace("{cwd}", str(cwd or temp_path))
part = part.replace("{skill_dir}", str(cwd or temp_path))
cmd.append(part)
# Execute — pipe stdin for agents that read from it (e.g., codex)
stdin_input = full_prompt if preset.get("uses_stdin") else None
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
cwd=str(cwd or temp_path),
input=stdin_input,
)
if result.returncode != 0:
logger.error(f"{self.agent_display} returned error code {result.returncode}")
if result.stderr and result.stderr.strip():
logger.error(f"{self.agent_display} stderr: {result.stderr.strip()}")
# Try to read output file first
resp_path = Path(resp_file)
if resp_path.exists():
return resp_path.read_text(encoding="utf-8")
# Try any JSON file in temp dir
for json_file in temp_path.glob("*.json"):
if json_file.name != "prompt.json":
return json_file.read_text(encoding="utf-8")
# Fall back to stdout (with agent-specific parsing)
if result.stdout and result.stdout.strip():
stdout = result.stdout.strip()
parser = preset.get("parse_output")
if parser == "kimi":
stdout = self._parse_kimi_output(stdout)
return stdout
logger.warning(f"⚠️ No output from {self.agent_display}")
return None
except subprocess.TimeoutExpired:
logger.warning(f"⚠️ {self.agent_display} timeout ({timeout}s)")
return None
except FileNotFoundError:
logger.warning(
f"⚠️ {self.agent_display} CLI not found. "
f"Install it or set SKILL_SEEKER_AGENT to a different agent."
)
return None
except Exception as e:
logger.error(f"{self.agent_display} error: {e}")
return None
@staticmethod
def _parse_kimi_output(raw_output: str) -> str:
"""Parse Kimi CLI --print mode output to extract text content.
Kimi's --print mode outputs structured lines like:
TurnBegin(...)
StepBegin(...)
TextPart(type='text', text='actual content')
ThinkPart(type='think', think='...')
This extracts the text= values from TextPart lines.
"""
import re
text_parts = re.findall(r"TextPart\(type='text', text='(.+?)'\)", raw_output)
if text_parts:
return "\n".join(text_parts)
# Fallback: return raw if no TextPart found
return raw_output
def is_available(self) -> bool:
"""Check if the configured agent/API is available."""
if self.mode == "api":
return self.client is not None
# LOCAL mode: check if CLI exists
preset = AGENT_PRESETS.get(self.agent)
if not preset:
return False
version_cmd = preset.get("version_check")
if not version_cmd:
return shutil.which(preset["command"][0]) is not None
try:
result = subprocess.run(
version_cmd,
capture_output=True,
text=True,
timeout=5,
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
@staticmethod
def detect_api_key() -> tuple[str | None, str | None]:
"""
Detect API key from environment variables.
Returns:
(api_key, provider) tuple. Provider is "anthropic", "moonshot", "google", or "openai".
Returns (None, None) if no key found.
"""
for env_var, provider in API_KEY_MAP.items():
key = os.environ.get(env_var, "").strip()
if key:
return key, provider
return None, None
@staticmethod
def get_model(provider: str = "anthropic") -> str:
"""
Get the model name for a provider.
Checks SKILL_SEEKER_MODEL env var first, then provider-specific env vars,
then falls back to defaults.
"""
# Global override
global_model = os.environ.get("SKILL_SEEKER_MODEL", "").strip()
if global_model:
return global_model
# Provider-specific env vars
provider_env_map = {
"anthropic": "ANTHROPIC_MODEL",
"moonshot": "MOONSHOT_MODEL",
"google": "GOOGLE_MODEL",
"openai": "OPENAI_MODEL",
}
env_var = provider_env_map.get(provider)
if env_var:
model = os.environ.get(env_var, "").strip()
if model:
return model
return DEFAULT_MODELS.get(provider, "claude-sonnet-4-20250514")
@staticmethod
def detect_default_target() -> str:
"""
Auto-detect the default --target platform from available API keys.
Returns platform name: "claude", "kimi", "gemini", "openai", or "markdown" (fallback).
"""
_, provider = AgentClient.detect_api_key()
if provider:
return PROVIDER_TARGET_MAP.get(provider, "markdown")
return "markdown"
def log_mode(self) -> None:
"""Log the current mode and agent for UX."""
if self.mode == "api":
logger.info(f"✅ AI enhancement enabled (using {self.provider} API)")
elif self.mode == "local":
logger.info(f"✅ AI enhancement enabled (using LOCAL mode - {self.agent_display})")
else:
logger.info(" AI enhancement disabled")

View File

@@ -13,23 +13,17 @@ Features:
- Identifies best practices
Modes:
- API mode: Uses Claude API (requires ANTHROPIC_API_KEY)
- LOCAL mode: Uses Claude Code CLI (no API key needed, uses your Claude Max plan)
- API mode: Uses AI API (Anthropic, Moonshot/Kimi, Google, OpenAI)
- LOCAL mode: Uses AI coding agent CLI (Claude Code, Kimi, Codex, Copilot, OpenCode)
- AUTO mode: Tries API first, falls back to LOCAL
Credits:
- Uses Claude AI (Anthropic) for analysis
- Graceful degradation if API unavailable
Uses AgentClient for all AI invocations — fully agent-agnostic.
"""
import json
import logging
import os
import subprocess
import tempfile
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
logger = logging.getLogger(__name__)
@@ -55,24 +49,25 @@ class AIAnalysis:
class AIEnhancer:
"""Base class for AI enhancement"""
"""Base class for AI enhancement — delegates to AgentClient for all AI calls."""
def __init__(self, api_key: str | None = None, enabled: bool = True, mode: str = "auto"):
def __init__(
self,
api_key: str | None = None,
enabled: bool = True,
mode: str = "auto",
agent: str | None = None,
):
"""
Initialize AI enhancer.
Args:
api_key: Anthropic API key (uses ANTHROPIC_API_KEY env if None)
api_key: API key (auto-detected from env if None)
enabled: Enable AI enhancement (default: True)
mode: Enhancement mode - "auto" (default), "api", or "local"
- "auto": Use API if key available, otherwise fall back to LOCAL
- "api": Force API mode (fails if no key)
- "local": Use Claude Code CLI (no API key needed)
agent: Local CLI agent name (e.g., "kimi", "claude")
"""
self.enabled = enabled
self.mode = mode
self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
self.client = None
# Get settings from config (with defaults)
if CONFIG_AVAILABLE:
@@ -80,161 +75,35 @@ class AIEnhancer:
self.local_batch_size = config.get_local_batch_size()
self.local_parallel_workers = config.get_local_parallel_workers()
else:
self.local_batch_size = 20 # Default
self.local_parallel_workers = 3 # Default
self.local_batch_size = 20
self.local_parallel_workers = 3
# Determine actual mode
if mode == "auto":
if self.api_key:
self.mode = "api"
# Initialize AgentClient
from skill_seekers.cli.agent_client import AgentClient
self._agent = AgentClient(mode=mode, api_key=api_key, agent=agent)
self.mode = self._agent.mode
self.client = self._agent.client # For backward compatibility
if self.enabled:
if self._agent.is_available():
self._agent.log_mode()
else:
# Fall back to LOCAL mode (Claude Code CLI)
self.mode = "local"
logger.info(" No API key found, using LOCAL mode (Claude Code CLI)")
if self.mode == "api" and self.enabled:
try:
import anthropic
# Support custom base_url for GLM-4.7 and other Claude-compatible APIs
client_kwargs = {"api_key": self.api_key}
base_url = os.environ.get("ANTHROPIC_BASE_URL")
if base_url:
client_kwargs["base_url"] = base_url
logger.info(f"✅ Using custom API base URL: {base_url}")
self.client = anthropic.Anthropic(**client_kwargs)
logger.info("✅ AI enhancement enabled (using Claude API)")
except ImportError:
logger.warning("⚠️ anthropic package not installed, falling back to LOCAL mode")
self.mode = "local"
except Exception as e:
logger.warning(
f"⚠️ Failed to initialize API client: {e}, falling back to LOCAL mode"
f"⚠️ {self._agent.agent_display} not available. AI enhancement disabled."
)
self.mode = "local"
if self.mode == "local" and self.enabled:
# Verify Claude CLI is available
if self._check_claude_cli():
logger.info("✅ AI enhancement enabled (using LOCAL mode - Claude Code CLI)")
else:
logger.warning("⚠️ Claude Code CLI not found. AI enhancement disabled.")
logger.warning(" Install with: npm install -g @anthropic-ai/claude-code")
self.enabled = False
def _check_claude_cli(self) -> bool:
"""Check if Claude Code CLI is available"""
try:
result = subprocess.run(
["claude", "--version"],
capture_output=True,
text=True,
timeout=5,
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def _call_claude(self, prompt: str, max_tokens: int = 1000) -> str | None:
"""Call Claude (API or LOCAL mode) with error handling"""
if self.mode == "api":
return self._call_claude_api(prompt, max_tokens)
elif self.mode == "local":
return self._call_claude_local(prompt)
return None
"""Call AI agent (API or LOCAL mode) with error handling.
def _call_claude_api(self, prompt: str, max_tokens: int = 1000) -> str | None:
"""Call Claude API"""
if not self.client:
return None
Named _call_claude for backward compatibility — delegates to AgentClient.
"""
return self._agent.call(prompt, max_tokens=max_tokens)
try:
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
except Exception as e:
logger.warning(f"⚠️ AI API call failed: {e}")
return None
def _call_claude_local(self, prompt: str) -> str | None:
"""Call Claude using LOCAL mode (Claude Code CLI)"""
try:
# Create a temporary directory for this enhancement
with tempfile.TemporaryDirectory(prefix="ai_enhance_") as temp_dir:
temp_path = Path(temp_dir)
# Create prompt file
prompt_file = temp_path / "prompt.md"
output_file = temp_path / "response.json"
# Write prompt with instructions to output JSON
full_prompt = f"""# AI Analysis Task
IMPORTANT: You MUST write your response as valid JSON to this file:
{output_file}
## Task
{prompt}
## Instructions
1. Analyze the input carefully
2. Generate the JSON response as specified
3. Use the Write tool to save the JSON to: {output_file}
4. The JSON must be valid and parseable
DO NOT include any explanation - just write the JSON file.
"""
prompt_file.write_text(full_prompt)
# Run Claude CLI
result = subprocess.run(
["claude", "--dangerously-skip-permissions", str(prompt_file)],
capture_output=True,
text=True,
timeout=120, # 2 minute timeout per call
cwd=str(temp_path),
)
if result.returncode != 0:
logger.warning(f"⚠️ Claude CLI returned error: {result.returncode}")
return None
# Read output file
if output_file.exists():
response_text = output_file.read_text()
# Try to extract JSON from response
try:
# Validate it's valid JSON
json.loads(response_text)
return response_text
except json.JSONDecodeError:
# Try to find JSON in the response
import re
json_match = re.search(r"\[[\s\S]*\]|\{[\s\S]*\}", response_text)
if json_match:
return json_match.group()
logger.warning("⚠️ Could not parse JSON from LOCAL response")
return None
else:
# Look for any JSON file created
for json_file in temp_path.glob("*.json"):
if json_file.name != "prompt.json":
return json_file.read_text()
logger.warning("⚠️ No output file from LOCAL mode")
return None
except subprocess.TimeoutExpired:
logger.warning("⚠️ Claude CLI timeout (2 minutes)")
return None
except Exception as e:
logger.warning(f"⚠️ LOCAL mode error: {e}")
return None
def call(self, prompt: str, max_tokens: int = 1000) -> str | None:
"""Call AI agent — preferred method name over _call_claude."""
return self._agent.call(prompt, max_tokens=max_tokens)
class PatternEnhancer(AIEnhancer):

View File

@@ -139,12 +139,13 @@ class ArchitecturalPatternDetector:
"Laravel": ["laravel", "illuminate", "artisan", "app/Http/Controllers", "app/Models"],
}
def __init__(self, enhance_with_ai: bool = True):
def __init__(self, enhance_with_ai: bool = True, agent: str | None = None):
"""
Initialize detector.
Args:
enhance_with_ai: Enable AI enhancement for detected patterns (C3.6)
agent: Local CLI agent name (e.g., "kimi", "claude")
"""
self.enhance_with_ai = enhance_with_ai
self.ai_enhancer = None
@@ -153,7 +154,7 @@ class ArchitecturalPatternDetector:
try:
from skill_seekers.cli.ai_enhancer import AIEnhancer
self.ai_enhancer = AIEnhancer()
self.ai_enhancer = AIEnhancer(agent=agent)
except Exception as e:
logger.warning(f"⚠️ Failed to initialize AI enhancer: {e}")
self.enhance_with_ai = False

View File

@@ -57,8 +57,8 @@ def add_asciidoc_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for AsciiDoc), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# AsciiDoc-specific args

View File

@@ -91,8 +91,8 @@ def add_chat_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for Chat), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# Chat-specific args

View File

@@ -55,7 +55,7 @@ COMMON_ARGUMENTS: dict[str, dict[str, Any]] = {
"help": (
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled, 1=SKILL.md only, 2=+architecture/config (default), 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), otherwise LOCAL (AI coding agent)"
),
"metavar": "LEVEL",
},
@@ -64,10 +64,27 @@ COMMON_ARGUMENTS: dict[str, dict[str, Any]] = {
"flags": ("--api-key",),
"kwargs": {
"type": str,
"help": "Anthropic API key for --enhance (or set ANTHROPIC_API_KEY env var)",
"help": "API key for enhancement (ANTHROPIC_API_KEY, GOOGLE_API_KEY, OPENAI_API_KEY, MOONSHOT_API_KEY)",
"metavar": "KEY",
},
},
"agent": {
"flags": ("--agent",),
"kwargs": {
"type": str,
"choices": ["claude", "codex", "copilot", "opencode", "kimi", "custom"],
"help": "Local coding agent for enhancement (default: AI agent from SKILL_SEEKER_AGENT env var)",
"metavar": "AGENT",
},
},
"agent_cmd": {
"flags": ("--agent-cmd",),
"kwargs": {
"type": str,
"help": "Override agent command template (advanced)",
"metavar": "CMD",
},
},
"doc_version": {
"flags": ("--doc-version",),
"kwargs": {

View File

@@ -98,8 +98,8 @@ def add_confluence_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for Confluence), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# Confluence-specific args

View File

@@ -57,7 +57,7 @@ UNIVERSAL_ARGUMENTS: dict[str, dict[str, Any]] = {
"help": (
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled, 1=SKILL.md only, 2=+architecture/config (default), 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), otherwise LOCAL (AI coding agent)"
),
"metavar": "LEVEL",
},
@@ -66,7 +66,7 @@ UNIVERSAL_ARGUMENTS: dict[str, dict[str, Any]] = {
"flags": ("--api-key",),
"kwargs": {
"type": str,
"help": "Anthropic API key (or set ANTHROPIC_API_KEY env var)",
"help": "API key for enhancement (ANTHROPIC_API_KEY, GOOGLE_API_KEY, OPENAI_API_KEY, MOONSHOT_API_KEY)",
"metavar": "KEY",
},
},
@@ -162,11 +162,30 @@ UNIVERSAL_ARGUMENTS: dict[str, dict[str, Any]] = {
"metavar": "VERSION",
},
},
# Agent selection for enhancement (added in v3.4.0 - multi-agent support)
"agent": {
"flags": ("--agent",),
"kwargs": {
"type": str,
"choices": ["claude", "codex", "copilot", "opencode", "kimi", "custom"],
"help": "Local coding agent for enhancement (default: AI agent from SKILL_SEEKER_AGENT env var)",
"metavar": "AGENT",
},
},
"agent_cmd": {
"flags": ("--agent-cmd",),
"kwargs": {
"type": str,
"help": "Override agent command template (advanced)",
"metavar": "CMD",
},
},
}
# Merge RAG arguments from common.py into universal arguments
UNIVERSAL_ARGUMENTS.update(RAG_ARGUMENTS)
# =============================================================================
# TIER 2: SOURCE-SPECIFIC ARGUMENTS
# =============================================================================
@@ -376,6 +395,43 @@ LOCAL_ARGUMENTS: dict[str, dict[str, Any]] = {
"help": "Skip documentation extraction",
},
},
"skip_api_reference": {
"flags": ("--skip-api-reference",),
"kwargs": {
"action": "store_true",
"help": "Skip API reference generation",
},
},
"skip_dependency_graph": {
"flags": ("--skip-dependency-graph",),
"kwargs": {
"action": "store_true",
"help": "Skip dependency graph analysis",
},
},
"skip_config_patterns": {
"flags": ("--skip-config-patterns",),
"kwargs": {
"action": "store_true",
"help": "Skip configuration pattern extraction",
},
},
"no_comments": {
"flags": ("--no-comments",),
"kwargs": {
"action": "store_true",
"help": "Skip comment extraction from source code",
},
},
"depth": {
"flags": ("--depth",),
"kwargs": {
"type": str,
"choices": ["surface", "deep", "full"],
"help": "Analysis depth (deprecated, use --preset instead)",
"metavar": "LEVEL",
},
},
}
# PDF specific (from pdf.py)
@@ -431,6 +487,13 @@ EPUB_ARGUMENTS: dict[str, dict[str, Any]] = {
# Video specific (from video.py)
VIDEO_ARGUMENTS: dict[str, dict[str, Any]] = {
"setup": {
"flags": ("--setup",),
"kwargs": {
"action": "store_true",
"help": "Auto-detect GPU and install video dependencies",
},
},
"video_url": {
"flags": ("--video-url",),
"kwargs": {
@@ -535,13 +598,14 @@ VIDEO_ARGUMENTS: dict[str, dict[str, Any]] = {
}
# Multi-source config specific (from unified_scraper.py)
# Note: --fresh is in WEB_ARGUMENTS, shared with config sources via dynamic forwarding
CONFIG_ARGUMENTS: dict[str, dict[str, Any]] = {
"merge_mode": {
"flags": ("--merge-mode",),
"kwargs": {
"type": str,
"choices": ["rule-based", "claude-enhanced"],
"help": "Override merge mode from config (rule-based or claude-enhanced)",
"choices": ["rule-based", "ai-enhanced", "claude-enhanced"],
"help": "Override merge mode from config (rule-based or ai-enhanced). 'claude-enhanced' accepted as alias.",
"metavar": "MODE",
},
},
@@ -677,6 +741,14 @@ CHAT_ARGUMENTS: dict[str, dict[str, Any]] = {
# Hidden from default help, shown only with --help-advanced
ADVANCED_ARGUMENTS: dict[str, dict[str, Any]] = {
"from_json": {
"flags": ("--from-json",),
"kwargs": {
"type": str,
"help": "Build skill from pre-extracted JSON data (skip scraping). Supported by: PDF, Video, Jupyter, HTML, OpenAPI, AsciiDoc, PPTX, RSS, Manpage, Confluence, Notion, Chat.",
"metavar": "PATH",
},
},
"no_rate_limit": {
"flags": ("--no-rate-limit",),
"kwargs": {

View File

@@ -22,12 +22,12 @@ ENHANCE_ARGUMENTS: dict[str, dict[str, Any]] = {
"flags": ("--target",),
"kwargs": {
"type": str,
"choices": ["claude", "gemini", "openai"],
"choices": ["claude", "gemini", "openai", "kimi"],
"help": (
"AI platform for enhancement (uses API mode). "
"Auto-detected from env vars if not specified: "
"ANTHROPIC_API_KEY->claude, GOOGLE_API_KEY->gemini, OPENAI_API_KEY->openai. "
"Falls back to LOCAL mode (Claude Code CLI) when no API keys are found."
"ANTHROPIC_API_KEY->claude, GOOGLE_API_KEY->gemini, OPENAI_API_KEY->openai, MOONSHOT_API_KEY->kimi. "
"Falls back to LOCAL mode (AI coding agent) when no API keys are found."
),
"metavar": "PLATFORM",
},
@@ -38,7 +38,7 @@ ENHANCE_ARGUMENTS: dict[str, dict[str, Any]] = {
"type": str,
"help": (
"API key for the target platform "
"(or set ANTHROPIC_API_KEY / GOOGLE_API_KEY / OPENAI_API_KEY)"
"(or set ANTHROPIC_API_KEY / GOOGLE_API_KEY / OPENAI_API_KEY / MOONSHOT_API_KEY)"
),
"metavar": "KEY",
},
@@ -55,8 +55,8 @@ ENHANCE_ARGUMENTS: dict[str, dict[str, Any]] = {
"flags": ("--agent",),
"kwargs": {
"type": str,
"choices": ["claude", "codex", "copilot", "opencode", "custom"],
"help": "Local coding agent to use (default: claude or SKILL_SEEKER_AGENT)",
"choices": ["claude", "codex", "copilot", "opencode", "kimi", "custom"],
"help": "Local coding agent to use (default: AI agent from SKILL_SEEKER_AGENT env var)",
"metavar": "AGENT",
},
},
@@ -101,8 +101,11 @@ ENHANCE_ARGUMENTS: dict[str, dict[str, Any]] = {
"flags": ("--timeout",),
"kwargs": {
"type": int,
"default": 600,
"help": "Timeout in seconds (default: 600)",
"default": None, # Resolved at runtime via get_default_timeout()
"help": (
"Timeout in seconds "
"(default: 45 minutes, set SKILL_SEEKER_ENHANCE_TIMEOUT to override)"
),
"metavar": "SECONDS",
},
},

View File

@@ -56,7 +56,7 @@ def add_epub_arguments(parser: argparse.ArgumentParser) -> None:
action.help = (
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for EPUB), 1=SKILL.md only, 2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), otherwise LOCAL (AI coding agent)"
)
# EPUB-specific args

View File

@@ -57,8 +57,8 @@ def add_html_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for HTML), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# HTML-specific args

View File

@@ -57,8 +57,8 @@ def add_jupyter_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for Jupyter), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# Jupyter-specific args

View File

@@ -73,8 +73,8 @@ def add_manpage_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for ManPage), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# ManPage-specific args

View File

@@ -90,8 +90,8 @@ def add_notion_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for Notion), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# Notion-specific args

View File

@@ -65,8 +65,8 @@ def add_openapi_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for OpenAPI), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# OpenAPI-specific args

View File

@@ -43,6 +43,14 @@ PACKAGE_ARGUMENTS: dict[str, dict[str, Any]] = {
"claude",
"gemini",
"openai",
"kimi",
"minimax",
"opencode",
"deepseek",
"qwen",
"openrouter",
"together",
"fireworks",
"markdown",
"langchain",
"llama-index",
@@ -53,8 +61,8 @@ PACKAGE_ARGUMENTS: dict[str, dict[str, Any]] = {
"qdrant",
"pinecone",
],
"default": "claude",
"help": "Target LLM platform (default: claude)",
"default": None,
"help": "Target LLM platform (auto-detected from API keys, or 'markdown' if none set)",
"metavar": "PLATFORM",
},
},
@@ -133,6 +141,32 @@ PACKAGE_ARGUMENTS: dict[str, dict[str, Any]] = {
"help": "Allow code block splitting (default: code blocks preserved)",
},
},
# Marketplace options
"marketplace": {
"flags": ("--marketplace",),
"kwargs": {
"type": str,
"default": None,
"help": "Publish to registered marketplace after packaging (use add_marketplace to register)",
"metavar": "NAME",
},
},
"marketplace_category": {
"flags": ("--marketplace-category",),
"kwargs": {
"type": str,
"default": "development",
"help": "Plugin category in marketplace (default: development)",
"metavar": "CAT",
},
},
"create_branch": {
"flags": ("--create-branch",),
"kwargs": {
"action": "store_true",
"help": "Create a feature branch in marketplace repo instead of committing to main",
},
},
}

View File

@@ -64,7 +64,7 @@ def add_pdf_arguments(parser: argparse.ArgumentParser) -> None:
action.help = (
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for PDF), 1=SKILL.md only, 2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), otherwise LOCAL (AI coding agent)"
)
# PDF-specific args

View File

@@ -57,8 +57,8 @@ def add_pptx_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for PPTX), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# PPTX-specific args

View File

@@ -90,8 +90,8 @@ def add_rss_arguments(parser: argparse.ArgumentParser) -> None:
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for RSS), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), "
"otherwise LOCAL (AI coding agent)"
)
# RSS-specific args

View File

@@ -22,7 +22,7 @@ UNIFIED_ARGUMENTS: dict[str, dict[str, Any]] = {
"flags": ("--merge-mode",),
"kwargs": {
"type": str,
"help": "Merge mode (rule-based, claude-enhanced)",
"help": "Merge mode (rule-based, ai-enhanced). 'claude-enhanced' is accepted as alias.",
"metavar": "MODE",
},
},
@@ -72,12 +72,30 @@ UNIFIED_ARGUMENTS: dict[str, dict[str, Any]] = {
"help": "Preview workflow stages without executing (requires --enhance-workflow)",
},
},
# Agent selection for LOCAL mode enhancement
"agent": {
"flags": ("--agent",),
"kwargs": {
"type": str,
"choices": ["claude", "codex", "copilot", "opencode", "kimi", "custom"],
"help": "Local coding agent for enhancement (default: AI agent from SKILL_SEEKER_AGENT env var)",
"metavar": "AGENT",
},
},
"agent_cmd": {
"flags": ("--agent-cmd",),
"kwargs": {
"type": str,
"help": "Override agent command template (use {prompt_file} or stdin)",
"metavar": "CMD",
},
},
# API key and enhance-level (parity with scrape/github/analyze/pdf)
"api_key": {
"flags": ("--api-key",),
"kwargs": {
"type": str,
"help": "Anthropic API key (or set ANTHROPIC_API_KEY env var)",
"help": "API key for enhancement (ANTHROPIC_API_KEY, GOOGLE_API_KEY, OPENAI_API_KEY, or MOONSHOT_API_KEY)",
"metavar": "KEY",
},
},

View File

@@ -22,9 +22,9 @@ UPLOAD_ARGUMENTS: dict[str, dict[str, Any]] = {
"flags": ("--target",),
"kwargs": {
"type": str,
"choices": ["claude", "gemini", "openai", "chroma", "weaviate"],
"default": "claude",
"help": "Target platform (default: claude)",
"choices": ["claude", "gemini", "openai", "kimi", "chroma", "weaviate"],
"default": None,
"help": "Target platform (auto-detected from API keys, or 'claude' if none set)",
"metavar": "PLATFORM",
},
},

View File

@@ -156,7 +156,7 @@ def add_video_arguments(parser: argparse.ArgumentParser) -> None:
action.help = (
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for video), 1=SKILL.md only, 2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), otherwise LOCAL (AI coding agent)"
)
# Video-specific args

View File

@@ -56,7 +56,7 @@ def add_word_arguments(parser: argparse.ArgumentParser) -> None:
action.help = (
"AI enhancement level (auto-detects API vs LOCAL mode): "
"0=disabled (default for Word), 1=SKILL.md only, 2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, otherwise LOCAL (Claude Code)"
"Mode selection: uses API if API key is set (ANTHROPIC_API_KEY, MOONSHOT_API_KEY, etc.), otherwise LOCAL (AI coding agent)"
)
# Word-specific args

View File

@@ -1059,13 +1059,17 @@ def main() -> int:
print("❌ API enhancement not available. Falling back to LOCAL mode...")
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
else:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")

View File

@@ -64,12 +64,21 @@ class BrowserRenderer:
html = renderer.render_page(url)
"""
def __init__(self, timeout: int = 30000, wait_until: str = "networkidle"):
def __init__(
self,
timeout: int = 60000,
wait_until: str = "domcontentloaded",
extra_wait: int = 0,
):
"""Initialize renderer.
Args:
timeout: Page load timeout in milliseconds (default: 30s)
timeout: Page load timeout in milliseconds (default: 60s)
wait_until: Playwright wait condition — "networkidle", "load", "domcontentloaded"
Default changed to "domcontentloaded" for better compatibility
with heavy sites (Unity docs, DocFX, etc.) that never reach networkidle.
extra_wait: Additional milliseconds to wait after page load for lazy-loaded
navigation/content (e.g., 5000 for DocFX sidebar). Default: 0.
"""
if not _check_playwright_available():
raise ImportError(
@@ -80,6 +89,7 @@ class BrowserRenderer:
self._timeout = timeout
self._wait_until = wait_until
self._extra_wait = extra_wait
self._playwright = None
self._browser = None
self._context = None
@@ -127,6 +137,8 @@ class BrowserRenderer:
page = self._context.new_page()
try:
page.goto(url, wait_until=self._wait_until, timeout=self._timeout)
if self._extra_wait > 0:
page.wait_for_timeout(self._extra_wait)
html = page.content()
return html
finally:

View File

@@ -1885,7 +1885,9 @@ Examples:
LocalSkillEnhancer,
)
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
else:
@@ -1893,7 +1895,9 @@ Examples:
LocalSkillEnhancer,
)
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")

View File

@@ -650,6 +650,7 @@ def process_markdown_docs(
gitignore_spec: pathspec.PathSpec | None = None,
enhance_with_ai: bool = False,
ai_mode: str = "none",
agent: str | None = None,
) -> dict[str, Any]:
"""
Process all markdown documentation files in a directory.
@@ -822,7 +823,7 @@ def process_markdown_docs(
if enhance_with_ai and ai_mode != "none" and processed_docs:
logger.info("🤖 Enhancing documentation analysis with AI...")
try:
processed_docs = _enhance_docs_with_ai(processed_docs, ai_mode)
processed_docs = _enhance_docs_with_ai(processed_docs, ai_mode, agent=agent)
logger.info("✅ AI documentation enhancement complete")
except Exception as e:
logger.warning(f"⚠️ AI enhancement failed: {e}")
@@ -898,55 +899,46 @@ def process_markdown_docs(
return index_data
def _enhance_docs_with_ai(docs: list[dict], ai_mode: str) -> list[dict]:
def _enhance_docs_with_ai(docs: list[dict], ai_mode: str, agent: str | None = None) -> list[dict]:
"""
Enhance documentation analysis with AI.
Enhance documentation analysis with AI via AgentClient.
Args:
docs: List of processed document dictionaries
ai_mode: AI mode ('api' or 'local')
ai_mode: AI mode ('api', 'local', or 'auto')
agent: Local CLI agent name (e.g., "kimi", "claude")
Returns:
Enhanced document list
"""
# Try API mode first
if ai_mode in ("api", "auto"):
api_key = os.environ.get("ANTHROPIC_API_KEY")
if api_key:
return _enhance_docs_api(docs, api_key)
from skill_seekers.cli.agent_client import AgentClient
# Fall back to LOCAL mode
if ai_mode in ("local", "auto"):
return _enhance_docs_local(docs)
client = AgentClient(mode=ai_mode, agent=agent)
return docs
if not client.is_available():
logger.warning("⚠️ No AI agent available for documentation enhancement")
return docs
# Batch documents for efficiency
batch_size = 10
docs_with_summary = [d for d in docs if d.get("summary")]
if not docs_with_summary:
return docs
def _enhance_docs_api(docs: list[dict], api_key: str) -> list[dict]:
"""Enhance docs using Claude API."""
try:
import anthropic
for i in range(0, len(docs_with_summary), batch_size):
batch = docs_with_summary[i : i + batch_size]
client = anthropic.Anthropic(api_key=api_key)
docs_text = "\n\n".join(
[
f"## {d.get('title', d['filename'])}\nCategory: {d['category']}\nSummary: {d.get('summary', 'N/A')}"
for d in batch
]
)
# Batch documents for efficiency
batch_size = 10
for i in range(0, len(docs), batch_size):
batch = docs[i : i + batch_size]
if not docs_text:
continue
# Create prompt for batch
docs_text = "\n\n".join(
[
f"## {d.get('title', d['filename'])}\nCategory: {d['category']}\nSummary: {d.get('summary', 'N/A')}"
for d in batch
if d.get("summary")
]
)
if not docs_text:
continue
prompt = f"""Analyze these documentation files and provide:
prompt = f"""Analyze these documentation files and provide:
1. A brief description of what each document covers
2. Key topics/concepts mentioned
3. How they relate to each other
@@ -957,15 +949,10 @@ Documents:
Return JSON with format:
{{"enhancements": [{{"filename": "...", "description": "...", "key_topics": [...], "related_to": [...]}}]}}"""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}],
)
# Parse response and merge enhancements
try:
json_match = re.search(r"\{.*\}", response.content[0].text, re.DOTALL)
try:
response = client.call(prompt, max_tokens=2000)
if response:
json_match = re.search(r"\{.*\}", response, re.DOTALL)
if json_match:
enhancements = json.loads(json_match.group())
for enh in enhancements.get("enhancements", []):
@@ -974,72 +961,8 @@ Return JSON with format:
doc["ai_description"] = enh.get("description")
doc["ai_topics"] = enh.get("key_topics", [])
doc["ai_related"] = enh.get("related_to", [])
except Exception:
pass
except Exception as e:
logger.warning(f"API enhancement failed: {e}")
return docs
def _enhance_docs_local(docs: list[dict]) -> list[dict]:
"""Enhance docs using Claude Code CLI (LOCAL mode)."""
import subprocess
import tempfile
# Prepare batch of docs for enhancement
docs_with_summary = [d for d in docs if d.get("summary")]
if not docs_with_summary:
return docs
docs_text = "\n\n".join(
[
f"## {d.get('title', d['filename'])}\nCategory: {d['category']}\nPath: {d['path']}\nSummary: {d.get('summary', 'N/A')}"
for d in docs_with_summary[:20] # Limit to 20 docs
]
)
prompt = f"""Analyze these documentation files from a codebase and provide insights.
For each document, provide:
1. A brief description of what it covers
2. Key topics/concepts
3. Related documents
Documents:
{docs_text}
Output JSON only:
{{"enhancements": [{{"filename": "...", "description": "...", "key_topics": ["..."], "related_to": ["..."]}}]}}"""
try:
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write(prompt)
prompt_file = f.name
result = subprocess.run(
["claude", "--dangerously-skip-permissions", "-p", prompt],
capture_output=True,
text=True,
timeout=120,
)
os.unlink(prompt_file)
if result.returncode == 0 and result.stdout:
json_match = re.search(r"\{.*\}", result.stdout, re.DOTALL)
if json_match:
enhancements = json.loads(json_match.group())
for enh in enhancements.get("enhancements", []):
for doc in docs:
if doc["filename"] == enh.get("filename"):
doc["ai_description"] = enh.get("description")
doc["ai_topics"] = enh.get("key_topics", [])
doc["ai_related"] = enh.get("related_to", [])
except Exception as e:
logger.warning(f"LOCAL enhancement failed: {e}")
except Exception as e:
logger.warning(f"AI enhancement batch failed: {e}")
return docs
@@ -1062,6 +985,8 @@ def analyze_codebase(
skill_name: str | None = None,
skill_description: str | None = None,
doc_version: str = "",
agent: str | None = None,
agent_cmd: str | None = None,
) -> dict[str, Any]:
"""
Analyze local codebase and extract code knowledge.
@@ -1467,7 +1392,7 @@ def analyze_codebase(
from skill_seekers.cli.config_enhancer import ConfigEnhancer
logger.info(f"🤖 Enhancing config analysis with AI (mode: {ai_mode})...")
enhancer = ConfigEnhancer(mode=ai_mode)
enhancer = ConfigEnhancer(mode=ai_mode, agent=agent)
result_dict = enhancer.enhance_config_result(result_dict)
logger.info("✅ AI enhancement complete")
except Exception as e:
@@ -1514,7 +1439,7 @@ def analyze_codebase(
logger.info("Analyzing architectural patterns...")
from skill_seekers.cli.architectural_pattern_detector import ArchitecturalPatternDetector
arch_detector = ArchitecturalPatternDetector(enhance_with_ai=enhance_architecture)
arch_detector = ArchitecturalPatternDetector(enhance_with_ai=enhance_architecture, agent=agent)
arch_report = arch_detector.analyze(directory, results["files"])
# Save architecture analysis if we have patterns OR frameworks (fixes #239)
@@ -1579,6 +1504,7 @@ def analyze_codebase(
gitignore_spec=gitignore_spec,
enhance_with_ai=enhance_docs_ai,
ai_mode=ai_mode,
agent=agent,
)
if docs_data and docs_data.get("total_files", 0) > 0:
@@ -1831,8 +1757,8 @@ def _get_language_stats(files: list[dict]) -> dict[str, int]:
def _format_patterns_section(output_dir: Path) -> str:
"""Format design patterns section from patterns/detected_patterns.json."""
patterns_file = output_dir / "patterns" / "detected_patterns.json"
"""Format design patterns section from patterns/all_patterns.json."""
patterns_file = output_dir / "patterns" / "all_patterns.json"
if not patterns_file.exists():
return ""
@@ -2315,8 +2241,8 @@ Examples:
help=(
"AI enhancement mode for how-to guides: "
"auto (auto-detect: API if ANTHROPIC_API_KEY set, else LOCAL), "
"api (Claude API, requires ANTHROPIC_API_KEY), "
"local (Claude Code Max, FREE, no API key), "
"api (Anthropic API, requires ANTHROPIC_API_KEY), "
"local (coding agent CLI, FREE, no API key), "
"none (disable AI enhancement). "
"💡 TIP: Use --enhance flag instead for simpler UX!"
),

View File

@@ -313,7 +313,7 @@ def api_keys_menu():
print("╚═══════════════════════════════════════════════════╝\n")
print("Current status:")
for provider in ["anthropic", "google", "openai"]:
for provider in ["anthropic", "google", "openai", "moonshot"]:
key = config.get_api_key(provider)
status = "✅ Set" if key else "❌ Not set"
source = ""
@@ -324,6 +324,7 @@ def api_keys_menu():
"anthropic": "ANTHROPIC_API_KEY",
"google": "GOOGLE_API_KEY",
"openai": "OPENAI_API_KEY",
"moonshot": "MOONSHOT_API_KEY",
}[provider]
source = " (from environment)" if os.getenv(env_var) else " (from config)"
print(f"{provider.capitalize()}: {status}{source}")
@@ -332,14 +333,16 @@ def api_keys_menu():
print(" 1. Set Anthropic (Claude) API Key")
print(" 2. Set Google (Gemini) API Key")
print(" 3. Set OpenAI (ChatGPT) API Key")
print(" 4. Set Moonshot (Kimi) API Key")
print(" 0. Back to Main Menu\n")
choice = input("Select an option [0-3]: ").strip()
choice = input("Select an option [0-4]: ").strip()
provider_map = {
"1": ("anthropic", "https://console.anthropic.com/settings/keys"),
"2": ("google", "https://makersuite.google.com/app/apikey"),
"3": ("openai", "https://platform.openai.com/api-keys"),
"4": ("moonshot", "https://platform.moonshot.cn/"),
}
if choice in provider_map:

View File

@@ -14,26 +14,15 @@ Similar to GuideEnhancer (C3.3) but for configuration files.
import json
import logging
import os
import subprocess
import sys
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from skill_seekers.cli.agent_client import AgentClient
# Configure logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
# Optional anthropic import
ANTHROPIC_AVAILABLE = False
try:
import anthropic
ANTHROPIC_AVAILABLE = True
except ImportError:
pass
@dataclass
class ConfigEnhancement:
@@ -62,51 +51,22 @@ class ConfigEnhancer:
AI enhancement for configuration extraction results.
Supports dual-mode operation:
- API mode: Uses Claude API (requires ANTHROPIC_API_KEY)
- LOCAL mode: Uses Claude Code CLI (no API key needed)
- API mode: Uses Anthropic API (requires ANTHROPIC_API_KEY)
- LOCAL mode: Uses a coding agent CLI (no API key needed)
- AUTO mode: Automatically detects best available mode
"""
def __init__(self, mode: str = "auto"):
def __init__(self, mode: str = "auto", agent: str | None = None):
"""
Initialize ConfigEnhancer.
Args:
mode: Enhancement mode - "api", "local", or "auto" (default)
agent: Local CLI agent name (e.g., "kimi", "claude")
"""
self.mode = self._detect_mode(mode)
self.api_key = os.environ.get("ANTHROPIC_API_KEY")
self.client = None
if self.mode == "api" and ANTHROPIC_AVAILABLE and self.api_key:
# Support custom base_url for GLM-4.7 and other Claude-compatible APIs
client_kwargs = {"api_key": self.api_key}
base_url = os.environ.get("ANTHROPIC_BASE_URL")
if base_url:
client_kwargs["base_url"] = base_url
logger.info(f"✅ Using custom API base URL: {base_url}")
self.client = anthropic.Anthropic(**client_kwargs)
def _detect_mode(self, requested_mode: str) -> str:
"""
Detect best enhancement mode.
Args:
requested_mode: User-requested mode
Returns:
Actual mode to use
"""
if requested_mode in ["api", "local"]:
return requested_mode
# Auto-detect
if os.environ.get("ANTHROPIC_API_KEY") and ANTHROPIC_AVAILABLE:
logger.info("🤖 AI enhancement: API mode (Claude API detected)")
return "api"
else:
logger.info("🤖 AI enhancement: LOCAL mode (using Claude Code CLI)")
return "local"
self._agent = AgentClient(mode=mode, agent=agent)
self.mode = self._agent.mode
self._agent.log_mode()
def enhance_config_result(self, result: dict) -> dict:
"""
@@ -126,29 +86,29 @@ class ConfigEnhancer:
return self._enhance_via_local(result)
# =========================================================================
# API MODE - Direct Claude API calls
# API MODE - Direct AI API calls
# =========================================================================
def _enhance_via_api(self, result: dict) -> dict:
"""Enhance configs using Claude API"""
if not self.client:
logger.error("❌ API mode requested but no API key available")
"""Enhance configs using AI API"""
if not self._agent.is_available():
logger.error("❌ API mode requested but no API client available")
return result
try:
# Create enhancement prompt
prompt = self._create_enhancement_prompt(result)
# Call Claude API
logger.info("📡 Calling Claude API for config analysis...")
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=8000,
messages=[{"role": "user", "content": prompt}],
)
# Call AI agent for config analysis
logger.info("📡 Calling AI agent for config analysis...")
response_text = self._agent.call(prompt, max_tokens=8000)
if not response_text:
logger.error("❌ AI agent returned no response")
return result
# Parse response
enhanced_result = self._parse_api_response(response.content[0].text, result)
enhanced_result = self._parse_api_response(response_text, result)
logger.info("✅ API enhancement complete")
return enhanced_result
@@ -157,7 +117,7 @@ class ConfigEnhancer:
return result
def _create_enhancement_prompt(self, result: dict) -> str:
"""Create prompt for Claude API"""
"""Create prompt for AI API"""
config_files = result.get("config_files", [])
# Summarize configs for prompt
@@ -217,7 +177,7 @@ Focus on actionable insights that help developers understand and improve their c
return prompt
def _parse_api_response(self, response_text: str, original_result: dict) -> dict:
"""Parse Claude API response and merge with original result"""
"""Parse AI API response and merge with original result"""
try:
# Extract JSON from response
import re
@@ -248,56 +208,54 @@ Focus on actionable insights that help developers understand and improve their c
return original_result
# =========================================================================
# LOCAL MODE - Claude Code CLI
# LOCAL MODE - Coding Agent CLI
# =========================================================================
def _enhance_via_local(self, result: dict) -> dict:
"""Enhance configs using Claude Code CLI"""
"""Enhance configs using LOCAL CLI agent"""
try:
# Create a temporary directory for this enhancement session
with tempfile.TemporaryDirectory(prefix="config_enhance_") as temp_dir:
temp_path = Path(temp_dir)
logger.info("🖥️ Launching LOCAL agent for config analysis...")
logger.info("⏱️ This will take 30-60 seconds...")
# Define output file path (absolute path that Claude will write to)
output_file = temp_path / "config_enhancement.json"
# Build the prompt — AgentClient handles output file management
prompt_content = self._create_local_prompt(result)
# Create prompt file with the output path embedded
prompt_file = temp_path / "enhance_prompt.md"
prompt_content = self._create_local_prompt(result, output_file)
prompt_file.write_text(prompt_content)
# Call via AgentClient which handles temp dirs, file polling, etc.
response_text = self._agent.call(prompt_content)
logger.info("🖥️ Launching Claude Code CLI for config analysis...")
logger.info("⏱️ This will take 30-60 seconds...")
if response_text:
try:
import re
# Run Claude Code CLI
result_data = self._run_claude_cli(prompt_file, output_file, temp_path)
json_match = re.search(r"\{.*\}", response_text, re.DOTALL)
if json_match:
data = json.loads(json_match.group())
if "file_enhancements" in data or "overall_insights" in data:
result["ai_enhancements"] = data
logger.info("✅ LOCAL enhancement complete")
return result
except json.JSONDecodeError as e:
logger.error(f"❌ Failed to parse LOCAL response as JSON: {e}")
if result_data:
# Merge LOCAL enhancements
result["ai_enhancements"] = result_data
logger.info("✅ LOCAL enhancement complete")
return result
else:
logger.warning("⚠️ LOCAL enhancement produced no results")
return result
logger.warning("⚠️ LOCAL enhancement produced no results")
return result
except Exception as e:
logger.error(f"❌ LOCAL enhancement failed: {e}")
return result
def _create_local_prompt(self, result: dict, output_file: Path) -> str:
"""Create prompt file for Claude Code CLI
def _create_local_prompt(self, result: dict) -> str:
"""Create prompt for AI agent.
Args:
result: Config extraction result dict
output_file: Absolute path where Claude should write the JSON output
Returns:
Prompt content string
"""
config_files = result.get("config_files", [])
# Format config data for Claude (limit to 15 files for reasonable prompt size)
# Format config data for AI agent (limit to 15 files for reasonable prompt size)
config_data = []
for cf in config_files[:15]:
# Support both "type" (from config_extractor) and "config_type" (legacy)
@@ -318,9 +276,6 @@ Focus on actionable insights that help developers understand and improve their c
prompt = f"""# Configuration Analysis Task
IMPORTANT: You MUST write the output to this EXACT file path:
{output_file}
## Configuration Files ({len(config_files)} total, showing first 15)
{chr(10).join(config_data)}
@@ -354,7 +309,7 @@ The JSON must have this EXACT structure:
## Instructions
1. Use the Write tool to create the JSON file at: {output_file}
1. Return the JSON response directly
2. Include an enhancement entry for each config file shown above
3. Focus on actionable insights:
- Explain what each config does in 1-2 sentences
@@ -366,97 +321,6 @@ DO NOT explain your work - just write the JSON file directly.
"""
return prompt
def _run_claude_cli(
self, prompt_file: Path, output_file: Path, working_dir: Path
) -> dict | None:
"""Run Claude Code CLI and wait for completion
Args:
prompt_file: Path to the prompt markdown file
output_file: Expected path where Claude will write the JSON output
working_dir: Working directory to run Claude from
Returns:
Parsed JSON dict if successful, None otherwise
"""
import time
try:
start_time = time.time()
# Run claude command with --dangerously-skip-permissions to bypass all prompts
# This allows Claude to write files without asking for confirmation
logger.info(f" Running: claude --dangerously-skip-permissions {prompt_file.name}")
logger.info(f" Output expected at: {output_file}")
result = subprocess.run(
["claude", "--dangerously-skip-permissions", str(prompt_file)],
capture_output=True,
text=True,
timeout=300, # 5 minute timeout
cwd=str(working_dir),
)
elapsed = time.time() - start_time
logger.info(f" Claude finished in {elapsed:.1f} seconds")
if result.returncode != 0:
logger.error(f"❌ Claude CLI failed (exit code {result.returncode})")
if result.stderr:
logger.error(f" Error: {result.stderr[:200]}")
return None
# Check if the expected output file was created
if output_file.exists():
try:
with open(output_file) as f:
data = json.load(f)
if "file_enhancements" in data or "overall_insights" in data:
logger.info(f"✅ Found enhancement data in {output_file.name}")
return data
else:
logger.warning("⚠️ Output file exists but missing expected keys")
except json.JSONDecodeError as e:
logger.error(f"❌ Failed to parse output JSON: {e}")
return None
# Fallback: Look for any JSON files created in the working directory
logger.info(" Looking for JSON files in working directory...")
current_time = time.time()
potential_files = []
for json_file in working_dir.glob("*.json"):
# Check if created recently (within last 2 minutes)
if current_time - json_file.stat().st_mtime < 120:
potential_files.append(json_file)
# Try to load the most recent JSON file with expected structure
for json_file in sorted(potential_files, key=lambda f: f.stat().st_mtime, reverse=True):
try:
with open(json_file) as f:
data = json.load(f)
if "file_enhancements" in data or "overall_insights" in data:
logger.info(f"✅ Found enhancement data in {json_file.name}")
return data
except Exception:
continue
logger.warning("⚠️ Could not find enhancement output file")
logger.info(f" Expected file: {output_file}")
logger.info(f" Files in dir: {list(working_dir.glob('*'))}")
return None
except subprocess.TimeoutExpired:
logger.error("❌ Claude CLI timeout (5 minutes)")
return None
except FileNotFoundError:
logger.error("'claude' command not found. Is Claude Code CLI installed?")
logger.error(" Install with: npm install -g @anthropic-ai/claude-code")
return None
except Exception as e:
logger.error(f"❌ Error running Claude CLI: {e}")
return None
def main():
"""Command-line interface for config enhancement"""

View File

@@ -271,13 +271,13 @@ class ConfigFileDetector:
".tmp",
}
def find_config_files(self, directory: Path, max_files: int = 100) -> list[ConfigFile]:
def find_config_files(self, directory: Path, max_files: int = 0) -> list[ConfigFile]:
"""
Find all configuration files in directory.
Args:
directory: Root directory to search
max_files: Maximum number of config files to find
max_files: Maximum number of config files to find (0 = unlimited)
Returns:
List of ConfigFile objects
@@ -286,7 +286,7 @@ class ConfigFileDetector:
found_count = 0
for file_path in self._walk_directory(directory):
if found_count >= max_files:
if max_files > 0 and found_count >= max_files:
logger.info(f"Reached max_files limit ({max_files})")
break
@@ -760,15 +760,13 @@ class ConfigExtractor:
self.parser = ConfigParser()
self.pattern_detector = ConfigPatternDetector()
def extract_from_directory(
self, directory: Path, max_files: int = 100
) -> ConfigExtractionResult:
def extract_from_directory(self, directory: Path, max_files: int = 0) -> ConfigExtractionResult:
"""
Extract configuration patterns from directory.
Args:
directory: Root directory to analyze
max_files: Maximum config files to process
max_files: Maximum config files to process (0 = unlimited)
Returns:
ConfigExtractionResult with all findings
@@ -857,7 +855,7 @@ def main():
parser.add_argument("directory", type=Path, help="Directory to analyze")
parser.add_argument("--output", "-o", type=Path, help="Output JSON file")
parser.add_argument(
"--max-files", type=int, default=100, help="Maximum config files to process"
"--max-files", type=int, default=0, help="Maximum config files to process (0 = unlimited)"
)
parser.add_argument(
"--enhance",
@@ -867,13 +865,13 @@ def main():
parser.add_argument(
"--enhance-local",
action="store_true",
help="Enhance with AI analysis (LOCAL mode, uses Claude Code CLI)",
help="Enhance with AI analysis (LOCAL mode, uses coding agent CLI)",
)
parser.add_argument(
"--ai-mode",
choices=["auto", "api", "local", "none"],
default="none",
help="AI enhancement mode: auto (detect), api (Claude API), local (Claude Code CLI), none (disable)",
help="AI enhancement mode: auto (detect), api (Anthropic API), local (coding agent CLI), none (disable)",
)
args = parser.parse_args()

View File

@@ -47,12 +47,12 @@ class ConfigManager:
"show_countdown": True,
},
"resume": {"auto_save_interval_seconds": 60, "keep_progress_days": 7},
"api_keys": {"anthropic": None, "google": None, "openai": None},
"api_keys": {"anthropic": None, "google": None, "openai": None, "moonshot": None},
"ai_enhancement": {
"default_enhance_level": 1, # Default AI enhancement level (0-3)
"default_agent": None, # "claude", "gemini", "openai", or None (auto-detect)
"local_batch_size": 20, # Patterns per Claude CLI call (default was 5)
"local_parallel_workers": 3, # Concurrent Claude CLI calls
"default_agent": None, # "claude", "gemini", "openai", "kimi", or None (auto-detect)
"local_batch_size": 20, # Patterns per CLI agent call (default was 5)
"local_parallel_workers": 3, # Concurrent CLI agent calls
},
"first_run": {"completed": False, "version": "2.7.0"},
}

View File

@@ -61,7 +61,11 @@ class ConfigValidator:
}
# Valid merge modes
VALID_MERGE_MODES = {"rule-based", "claude-enhanced"}
VALID_MERGE_MODES = {
"rule-based",
"ai-enhanced",
"claude-enhanced",
} # claude-enhanced kept as alias
# Valid code analysis depth levels
VALID_DEPTH_LEVELS = {"surface", "deep", "full"}
@@ -161,6 +165,21 @@ class ConfigValidator:
f"Invalid merge_mode: '{merge_mode}'. Must be one of {self.VALID_MERGE_MODES}"
)
# Validate marketplace_targets (optional)
marketplace_targets = self.config.get("marketplace_targets")
if marketplace_targets is not None:
if not isinstance(marketplace_targets, list):
raise ValueError("'marketplace_targets' must be an array")
for i, mt in enumerate(marketplace_targets):
if not isinstance(mt, dict):
raise ValueError(f"marketplace_targets[{i}]: must be an object")
if "marketplace" not in mt:
raise ValueError(
f"marketplace_targets[{i}]: missing required field 'marketplace'"
)
if not isinstance(mt["marketplace"], str):
raise ValueError(f"marketplace_targets[{i}]: 'marketplace' must be a string")
# Validate each source
for i, source in enumerate(sources):
self._validate_source(source, i)

View File

@@ -1962,7 +1962,7 @@ def main() -> int:
"0=disabled (default for Confluence), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"otherwise LOCAL (Claude Code, Kimi, etc.)"
)
# Confluence-specific arguments
@@ -2137,7 +2137,9 @@ def main() -> int:
LocalSkillEnhancer,
)
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print(" Local enhancement complete!")
else:
@@ -2145,7 +2147,9 @@ def main() -> int:
LocalSkillEnhancer,
)
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print(" Local enhancement complete!")

View File

@@ -28,7 +28,7 @@ CONTENT_MATCH_POINTS = 1 # points for content keyword match
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 enhancement limits (uses coding agent CLI)
LOCAL_CONTENT_LIMIT = 50000 # max characters for local enhancement
LOCAL_PREVIEW_LIMIT = 20000 # max characters for preview

View File

@@ -104,12 +104,20 @@ class CreateCommand:
if arg_value is None:
return False
# Check against common defaults
# Check against common defaults — args with these values were NOT
# explicitly set by the user and should not be forwarded.
defaults = {
"max_issues": 100,
"chunk_tokens": DEFAULT_CHUNK_TOKENS,
"chunk_overlap_tokens": DEFAULT_CHUNK_OVERLAP_TOKENS,
"output": None,
"doc_version": "",
"video_languages": "en",
"whisper_model": "base",
"platform": "slack",
"visual_interval": 0.7,
"visual_min_gap": 0.5,
"visual_similarity": 3.0,
}
if arg_name in defaults:
@@ -164,436 +172,227 @@ class CreateCommand:
logger.error(f"Unknown source type: {self.source_info.type}")
return 1
# ── Dynamic argument forwarding ──────────────────────────────────────
#
# Instead of manually checking each flag in every _route_*() method,
# _build_argv() dynamically iterates vars(self.args) and forwards all
# explicitly-set arguments. This is the same pattern used by
# main.py::_reconstruct_argv() and eliminates ~40 missing-flag gaps.
# Dest names that differ from their CLI flag (dest → flag)
_DEST_TO_FLAG = {
"async_mode": "--async",
"video_url": "--url",
"video_playlist": "--playlist",
"video_languages": "--languages",
"skip_config": "--skip-config-patterns",
}
# Internal args that should never be forwarded to sub-scrapers.
# video_url/video_playlist/video_file are handled as positionals by _route_video().
# config is forwarded manually only by routes that need it (web, github).
_SKIP_ARGS = frozenset(
{
"source",
"func",
"subcommand",
"command",
"config",
"video_url",
"video_playlist",
"video_file",
}
)
def _build_argv(
self,
module_name: str,
positional_args: list[str],
allowlist: frozenset[str] | None = None,
) -> list[str]:
"""Build argv dynamically by forwarding all explicitly-set arguments.
Uses the same pattern as main.py::_reconstruct_argv().
Replaces manual per-flag checking in _route_*() and _add_common_args().
Args:
module_name: Scraper module name (e.g., "doc_scraper")
positional_args: Positional arguments to prepend (e.g., [url] or ["--repo", repo])
allowlist: If provided, ONLY forward args in this set (overrides _SKIP_ARGS).
Used for targets with strict arg sets like unified_scraper.
Returns:
Complete argv list for the scraper
"""
argv = [module_name] + positional_args
# Auto-add suggested name if user didn't provide one (skip for allowlisted targets)
if not allowlist and not self.args.name and self.source_info:
argv.extend(["--name", self.source_info.suggested_name])
for key, value in vars(self.args).items():
# If allowlist provided, only forward args in the allowlist
if allowlist is not None:
if key not in allowlist:
continue
elif key in self._SKIP_ARGS or key.startswith("_help_"):
continue
if not self._is_explicitly_set(key, value):
continue
# Use translation map for mismatched dest→flag names, else derive from key
if key in self._DEST_TO_FLAG:
arg_flag = self._DEST_TO_FLAG[key]
else:
arg_flag = f"--{key.replace('_', '-')}"
if isinstance(value, bool):
if value:
argv.append(arg_flag)
elif isinstance(value, list):
for item in value:
argv.extend([arg_flag, str(item)])
elif value is not None:
argv.extend([arg_flag, str(value)])
return argv
def _call_module(self, module, argv: list[str]) -> int:
"""Call a scraper module with the given argv.
Swaps sys.argv, calls module.main(), restores sys.argv.
"""
logger.debug(f"Calling {argv[0]} with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
result = module.main()
if result is None:
logger.warning(f"Module returned None exit code, treating as success")
return 0
return result
finally:
sys.argv = original_argv
def _route_web(self) -> int:
"""Route to web documentation scraper (doc_scraper.py)."""
from skill_seekers.cli import doc_scraper
# Reconstruct argv for doc_scraper
argv = ["doc_scraper"]
url = self.source_info.parsed.get("url", self.source_info.raw_source)
argv = self._build_argv("doc_scraper", [url])
# Add URL
url = self.source_info.parsed["url"]
argv.append(url)
# Add universal arguments
self._add_common_args(argv)
# Config file (web-specific — loads selectors, categories, etc.)
# Forward config if set (not in _build_argv since it's in SKIP_ARGS
# to avoid double-forwarding for config-type sources)
if self.args.config:
argv.extend(["--config", self.args.config])
# RAG arguments (web scraper only)
if getattr(self.args, "chunk_for_rag", False):
argv.append("--chunk-for-rag")
if (
getattr(self.args, "chunk_tokens", None)
and self.args.chunk_tokens != DEFAULT_CHUNK_TOKENS
):
argv.extend(["--chunk-tokens", str(self.args.chunk_tokens)])
if (
getattr(self.args, "chunk_overlap_tokens", None)
and self.args.chunk_overlap_tokens != DEFAULT_CHUNK_OVERLAP_TOKENS
):
argv.extend(["--chunk-overlap-tokens", str(self.args.chunk_overlap_tokens)])
# Advanced web-specific arguments
if getattr(self.args, "no_preserve_code_blocks", False):
argv.append("--no-preserve-code-blocks")
if getattr(self.args, "no_preserve_paragraphs", False):
argv.append("--no-preserve-paragraphs")
if getattr(self.args, "interactive_enhancement", False):
argv.append("--interactive-enhancement")
# Web-specific arguments
if getattr(self.args, "max_pages", None):
argv.extend(["--max-pages", str(self.args.max_pages)])
if getattr(self.args, "skip_scrape", False):
argv.append("--skip-scrape")
if getattr(self.args, "resume", False):
argv.append("--resume")
if getattr(self.args, "fresh", False):
argv.append("--fresh")
if getattr(self.args, "rate_limit", None):
argv.extend(["--rate-limit", str(self.args.rate_limit)])
if getattr(self.args, "workers", None):
argv.extend(["--workers", str(self.args.workers)])
if getattr(self.args, "async_mode", False):
argv.append("--async")
if getattr(self.args, "no_rate_limit", False):
argv.append("--no-rate-limit")
if getattr(self.args, "browser", False):
argv.append("--browser")
# Call doc_scraper with modified argv
logger.debug(f"Calling doc_scraper with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
return doc_scraper.main()
finally:
sys.argv = original_argv
return self._call_module(doc_scraper, argv)
def _route_github(self) -> int:
"""Route to GitHub repository scraper (github_scraper.py)."""
from skill_seekers.cli import github_scraper
# Reconstruct argv for github_scraper
argv = ["github_scraper"]
repo = self.source_info.parsed.get("repo", self.source_info.raw_source)
argv = self._build_argv("github_scraper", ["--repo", repo])
# Add repo
repo = self.source_info.parsed["repo"]
argv.extend(["--repo", repo])
# Add universal arguments
self._add_common_args(argv)
# Config file (github-specific)
if self.args.config:
argv.extend(["--config", self.args.config])
# Add GitHub-specific arguments
if getattr(self.args, "token", None):
argv.extend(["--token", self.args.token])
if getattr(self.args, "profile", None):
argv.extend(["--profile", self.args.profile])
if getattr(self.args, "non_interactive", False):
argv.append("--non-interactive")
if getattr(self.args, "no_issues", False):
argv.append("--no-issues")
if getattr(self.args, "no_changelog", False):
argv.append("--no-changelog")
if getattr(self.args, "no_releases", False):
argv.append("--no-releases")
if getattr(self.args, "max_issues", None) and self.args.max_issues != 100:
argv.extend(["--max-issues", str(self.args.max_issues)])
if getattr(self.args, "scrape_only", False):
argv.append("--scrape-only")
if getattr(self.args, "local_repo_path", None):
argv.extend(["--local-repo-path", self.args.local_repo_path])
# Call github_scraper with modified argv
logger.debug(f"Calling github_scraper with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
return github_scraper.main()
finally:
sys.argv = original_argv
return self._call_module(github_scraper, argv)
def _route_local(self) -> int:
"""Route to local codebase analyzer (codebase_scraper.py)."""
from skill_seekers.cli import codebase_scraper
# Reconstruct argv for codebase_scraper
argv = ["codebase_scraper"]
# Add directory
directory = self.source_info.parsed["directory"]
argv.extend(["--directory", directory])
# Add universal arguments
self._add_common_args(argv)
# Preset (local codebase scraper has preset support)
if getattr(self.args, "preset", None):
argv.extend(["--preset", self.args.preset])
# Add local-specific arguments
if getattr(self.args, "languages", None):
argv.extend(["--languages", self.args.languages])
if getattr(self.args, "file_patterns", None):
argv.extend(["--file-patterns", self.args.file_patterns])
if getattr(self.args, "skip_patterns", False):
argv.append("--skip-patterns")
if getattr(self.args, "skip_test_examples", False):
argv.append("--skip-test-examples")
if getattr(self.args, "skip_how_to_guides", False):
argv.append("--skip-how-to-guides")
if getattr(self.args, "skip_config", False):
argv.append("--skip-config")
if getattr(self.args, "skip_docs", False):
argv.append("--skip-docs")
# Call codebase_scraper with modified argv
logger.debug(f"Calling codebase_scraper with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
return codebase_scraper.main()
finally:
sys.argv = original_argv
directory = self.source_info.parsed.get("directory", self.source_info.raw_source)
argv = self._build_argv("codebase_scraper", ["--directory", directory])
return self._call_module(codebase_scraper, argv)
def _route_pdf(self) -> int:
"""Route to PDF scraper (pdf_scraper.py)."""
from skill_seekers.cli import pdf_scraper
# Reconstruct argv for pdf_scraper
argv = ["pdf_scraper"]
# Add PDF file
file_path = self.source_info.parsed["file_path"]
argv.extend(["--pdf", file_path])
# Add universal arguments
self._add_common_args(argv)
# Add PDF-specific arguments
if getattr(self.args, "ocr", False):
argv.append("--ocr")
if getattr(self.args, "pages", None):
argv.extend(["--pages", self.args.pages])
# Call pdf_scraper with modified argv
logger.debug(f"Calling pdf_scraper with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
return pdf_scraper.main()
finally:
sys.argv = original_argv
file_path = self.source_info.parsed.get("file_path", self.source_info.raw_source)
argv = self._build_argv("pdf_scraper", ["--pdf", file_path])
return self._call_module(pdf_scraper, argv)
def _route_word(self) -> int:
"""Route to Word document scraper (word_scraper.py)."""
from skill_seekers.cli import word_scraper
# Reconstruct argv for word_scraper
argv = ["word_scraper"]
# Add DOCX file
file_path = self.source_info.parsed["file_path"]
argv.extend(["--docx", file_path])
# Add universal arguments
self._add_common_args(argv)
# Call word_scraper with modified argv
logger.debug(f"Calling word_scraper with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
return word_scraper.main()
finally:
sys.argv = original_argv
file_path = self.source_info.parsed.get("file_path", self.source_info.raw_source)
argv = self._build_argv("word_scraper", ["--docx", file_path])
return self._call_module(word_scraper, argv)
def _route_epub(self) -> int:
"""Route to EPUB scraper (epub_scraper.py)."""
from skill_seekers.cli import epub_scraper
# Reconstruct argv for epub_scraper
argv = ["epub_scraper"]
# Add EPUB file
file_path = self.source_info.parsed["file_path"]
argv.extend(["--epub", file_path])
# Add universal arguments
self._add_common_args(argv)
# Call epub_scraper with modified argv
logger.debug(f"Calling epub_scraper with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
return epub_scraper.main()
finally:
sys.argv = original_argv
file_path = self.source_info.parsed.get("file_path", self.source_info.raw_source)
argv = self._build_argv("epub_scraper", ["--epub", file_path])
return self._call_module(epub_scraper, argv)
def _route_video(self) -> int:
"""Route to video scraper (video_scraper.py)."""
from skill_seekers.cli import video_scraper
# Reconstruct argv for video_scraper
argv = ["video_scraper"]
# Add video source (URL or file)
parsed = self.source_info.parsed
video_playlist = getattr(self.args, "video_playlist", None)
if parsed.get("source_kind") == "file":
argv.extend(["--video-file", parsed["file_path"]])
elif video_playlist:
# Explicit --video-playlist flag takes precedence
argv.extend(["--playlist", video_playlist])
positional = ["--video-file", parsed["file_path"]]
elif parsed.get("url"):
url = parsed["url"]
# Detect playlist vs single video
if "playlist" in url.lower():
argv.extend(["--playlist", url])
else:
argv.extend(["--url", url])
flag = "--playlist" if "playlist" in url.lower() else "--url"
positional = [flag, url]
else:
positional = []
# Add universal arguments
self._add_common_args(argv)
argv = self._build_argv("video_scraper", positional)
return self._call_module(video_scraper, argv)
# Add video-specific arguments
video_langs = getattr(self.args, "video_languages", None) or getattr(
self.args, "languages", None
)
if video_langs:
argv.extend(["--languages", video_langs])
if getattr(self.args, "visual", False):
argv.append("--visual")
if getattr(self.args, "vision_ocr", False):
argv.append("--vision-ocr")
if getattr(self.args, "whisper_model", None) and self.args.whisper_model != "base":
argv.extend(["--whisper-model", self.args.whisper_model])
vi = getattr(self.args, "visual_interval", None)
if vi is not None and vi != 0.7:
argv.extend(["--visual-interval", str(vi)])
vmg = getattr(self.args, "visual_min_gap", None)
if vmg is not None and vmg != 0.5:
argv.extend(["--visual-min-gap", str(vmg)])
vs = getattr(self.args, "visual_similarity", None)
if vs is not None and vs != 3.0:
argv.extend(["--visual-similarity", str(vs)])
st = getattr(self.args, "start_time", None)
if st is not None:
argv.extend(["--start-time", str(st)])
et = getattr(self.args, "end_time", None)
if et is not None:
argv.extend(["--end-time", str(et)])
# Call video_scraper with modified argv
logger.debug(f"Calling video_scraper with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
return video_scraper.main()
finally:
sys.argv = original_argv
# Args accepted by unified_scraper (allowlist for config route)
_UNIFIED_SCRAPER_ARGS = frozenset(
{
"merge_mode",
"skip_codebase_analysis",
"fresh",
"dry_run",
"enhance_workflow",
"enhance_stage",
"var",
"workflow_dry_run",
"api_key",
"enhance_level",
"agent",
"agent_cmd",
}
)
def _route_config(self) -> int:
"""Route to unified scraper for config files (unified_scraper.py)."""
from skill_seekers.cli import unified_scraper
# Reconstruct argv for unified_scraper
argv = ["unified_scraper"]
# Add config file
config_path = self.source_info.parsed["config_path"]
argv.extend(["--config", config_path])
# Behavioral flags supported by unified_scraper
# Note: name/output/enhance-level come from the JSON config file, not CLI
if self.args.dry_run:
argv.append("--dry-run")
if getattr(self.args, "fresh", False):
argv.append("--fresh")
# Config-specific flags (--merge-mode, --skip-codebase-analysis)
if getattr(self.args, "merge_mode", None):
argv.extend(["--merge-mode", self.args.merge_mode])
if getattr(self.args, "skip_codebase_analysis", False):
argv.append("--skip-codebase-analysis")
# Enhancement workflow flags (unified_scraper now supports these)
if getattr(self.args, "enhance_workflow", None):
for wf in self.args.enhance_workflow:
argv.extend(["--enhance-workflow", wf])
if getattr(self.args, "enhance_stage", None):
for stage in self.args.enhance_stage:
argv.extend(["--enhance-stage", stage])
if getattr(self.args, "var", None):
for var in self.args.var:
argv.extend(["--var", var])
if getattr(self.args, "workflow_dry_run", False):
argv.append("--workflow-dry-run")
# Call unified_scraper with modified argv
logger.debug(f"Calling unified_scraper with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
return unified_scraper.main()
finally:
sys.argv = original_argv
argv = self._build_argv(
"unified_scraper",
["--config", config_path],
allowlist=self._UNIFIED_SCRAPER_ARGS,
)
return self._call_module(unified_scraper, argv)
def _route_generic(self, module_name: str, file_flag: str) -> int:
"""Generic routing for new source types.
Most new source types (jupyter, html, openapi, asciidoc, pptx, rss,
manpage, confluence, notion, chat) follow the same pattern:
import module, build argv with --flag <file_path>, add common args, call main().
Args:
module_name: Python module name under skill_seekers.cli (e.g., "jupyter_scraper")
file_flag: CLI flag for the source file (e.g., "--notebook")
Returns:
Exit code from scraper
All new source types (jupyter, html, openapi, asciidoc, pptx, rss,
manpage, confluence, notion, chat) use dynamic argument forwarding.
"""
import importlib
module = importlib.import_module(f"skill_seekers.cli.{module_name}")
argv = [module_name]
file_path = self.source_info.parsed.get("file_path", "")
if file_path:
argv.extend([file_flag, file_path])
self._add_common_args(argv)
logger.debug(f"Calling {module_name} with argv: {argv}")
original_argv = sys.argv
try:
sys.argv = argv
return module.main()
finally:
sys.argv = original_argv
def _add_common_args(self, argv: list[str]) -> None:
"""Add truly universal arguments to argv list.
These flags are accepted by ALL scrapers (doc, github, codebase, pdf)
because each scraper calls ``add_all_standard_arguments(parser)``
which registers: name, description, output, enhance-level, api-key,
dry-run, verbose, quiet, and workflow args.
Route-specific flags (preset, config, RAG, preserve, etc.) are
forwarded only by the _route_*() method that needs them.
"""
# Identity arguments
if self.args.name:
argv.extend(["--name", self.args.name])
elif hasattr(self, "source_info") and self.source_info:
# Use suggested name from source detection
argv.extend(["--name", self.source_info.suggested_name])
if self.args.description:
argv.extend(["--description", self.args.description])
if self.args.output:
argv.extend(["--output", self.args.output])
# Enhancement arguments (consolidated to --enhance-level only)
if self.args.enhance_level > 0:
argv.extend(["--enhance-level", str(self.args.enhance_level)])
if self.args.api_key:
argv.extend(["--api-key", self.args.api_key])
# Behavior arguments
if self.args.dry_run:
argv.append("--dry-run")
if self.args.verbose:
argv.append("--verbose")
if self.args.quiet:
argv.append("--quiet")
# Documentation version metadata
if getattr(self.args, "doc_version", ""):
argv.extend(["--doc-version", self.args.doc_version])
# Enhancement Workflow arguments
if getattr(self.args, "enhance_workflow", None):
for wf in self.args.enhance_workflow:
argv.extend(["--enhance-workflow", wf])
if getattr(self.args, "enhance_stage", None):
for stage in self.args.enhance_stage:
argv.extend(["--enhance-stage", stage])
if getattr(self.args, "var", None):
for var in self.args.var:
argv.extend(["--var", var])
if getattr(self.args, "workflow_dry_run", False):
argv.append("--workflow-dry-run")
positional = [file_flag, file_path] if file_path else []
argv = self._build_argv(module_name, positional)
return self._call_module(module, argv)
def main() -> int:

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Documentation to Claude Skill Converter
Single tool to scrape any documentation and create high-quality Claude skills.
Documentation to AI Skill Converter
Single tool to scrape any documentation and create high-quality AI skills.
Usage:
skill-seekers scrape --interactive
@@ -187,6 +187,8 @@ class DocToSkillConverter:
# Browser rendering mode (for JavaScript SPA sites)
self.browser_mode = config.get("browser", False)
self._browser_renderer = None
self._browser_wait_until = config.get("browser_wait_until", "domcontentloaded")
self._browser_extra_wait = config.get("browser_extra_wait", 0) # ms
# Parallel scraping config
self.workers = config.get("workers", 1)
@@ -250,7 +252,10 @@ class DocToSkillConverter:
Returns:
bool: True if URL matches include patterns and doesn't match exclude patterns
"""
if not url.startswith(self.base_url):
# Use directory part of base_url for prefix check so sibling pages match.
# e.g., base_url "https://example.com/docs/index.html" → prefix "https://example.com/docs/"
base_dir = self.base_url if self.base_url.endswith("/") else self.base_url + "/"
if not url.startswith(base_dir):
return False
if self._include_patterns and not any(pattern in url for pattern in self._include_patterns):
@@ -730,8 +735,14 @@ class DocToSkillConverter:
if self._browser_renderer is None:
from skill_seekers.cli.browser_renderer import BrowserRenderer
self._browser_renderer = BrowserRenderer()
logger.info("Launched headless browser for JavaScript rendering")
self._browser_renderer = BrowserRenderer(
wait_until=self._browser_wait_until,
extra_wait=self._browser_extra_wait,
)
logger.info(
f"Launched headless browser for JavaScript rendering "
f"(wait_until={self._browser_wait_until})"
)
return self._browser_renderer.render_page(url)
def scrape_page(self, url: str) -> None:
@@ -1092,6 +1103,126 @@ class DocToSkillConverter:
return True
def _try_sitemap(self) -> list[str]:
"""Layer 1: Try to discover pages via sitemap.xml.
Checks common sitemap locations at the domain root.
Parses XML for <loc> tags, filters by is_valid_url().
Returns:
List of discovered valid URLs (empty if no sitemap found).
"""
try:
import defusedxml.ElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
from urllib.parse import urlparse
parsed = urlparse(self.base_url)
domain = f"{parsed.scheme}://{parsed.netloc}"
sitemap_urls_to_try = [
f"{domain}/sitemap.xml",
f"{domain}/sitemap_index.xml",
]
discovered = []
for sitemap_url in sitemap_urls_to_try:
try:
response = requests.get(
sitemap_url, timeout=10, headers={"User-Agent": "SkillSeekers/3.4"}
)
if response.status_code != 200:
continue
if "xml" not in response.headers.get("content-type", ""):
continue
root = ET.fromstring(response.text)
ns = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
# Handle sitemap index (nested sitemaps)
for sitemap in root.findall(".//sm:sitemap/sm:loc", ns):
try:
sub_resp = requests.get(
sitemap.text.strip(),
timeout=10,
headers={"User-Agent": "SkillSeekers/3.4"},
)
if sub_resp.status_code == 200:
sub_root = ET.fromstring(sub_resp.text)
for loc in sub_root.findall(".//sm:url/sm:loc", ns):
url = loc.text.strip().split("#")[0]
if self.is_valid_url(url):
discovered.append(url)
except Exception:
continue
# Handle direct sitemap
for loc in root.findall(".//sm:url/sm:loc", ns):
url = loc.text.strip().split("#")[0]
if self.is_valid_url(url):
discovered.append(url)
if discovered:
logger.info(f"📋 Found sitemap at {sitemap_url} ({len(discovered)} valid URLs)")
return list(set(discovered))
except Exception as e:
logger.debug(f"Sitemap check failed for {sitemap_url}: {e}")
continue
return []
def _discover_spa_nav(self) -> list[str]:
"""Layer 3: Render index page with networkidle to discover SPA navigation.
Used when browser mode is on and sitemap/llms.txt didn't find pages.
Renders the first page with networkidle (slower but discovers full nav),
then normal crawl uses domcontentloaded (fast).
Returns:
List of discovered valid URLs from the rendered navigation.
"""
if not self.browser_mode:
return []
logger.info("🌐 Rendering index page with networkidle to discover SPA navigation...")
try:
from skill_seekers.cli.browser_renderer import BrowserRenderer
# Use a separate renderer with networkidle for discovery only
discovery_renderer = BrowserRenderer(
timeout=60000,
wait_until="networkidle",
extra_wait=3000, # 3s extra for lazy-loaded nav
)
html = discovery_renderer.render_page(self.base_url)
discovery_renderer.close()
# Parse rendered DOM for all links
soup = BeautifulSoup(html, "html.parser")
discovered = []
seen = set()
for link in soup.find_all("a", href=True):
href = urljoin(self.base_url, link["href"]).split("#")[0]
if href not in seen and self.is_valid_url(href):
seen.add(href)
discovered.append(href)
if discovered:
logger.info(f"🌐 Discovered {len(discovered)} pages from rendered SPA navigation")
return discovered
except Exception as e:
logger.warning(f"⚠️ SPA navigation discovery failed: {e}")
return []
def scrape_all(self) -> None:
"""Scrape all pages (supports llms.txt and HTML scraping)
@@ -1102,16 +1233,35 @@ class DocToSkillConverter:
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
# === Three-Layer Discovery Engine ===
# Discovers pages before the BFS crawl loop starts.
# Layer 1: sitemap.xml — instant, no rendering needed
# Layer 2: llms.txt — existing mechanism
# Layer 3: SPA nav — renders index with networkidle to find JS-rendered links
if not self.dry_run:
# Layer 1: Try sitemap.xml
sitemap_urls = self._try_sitemap()
if sitemap_urls:
for url in sitemap_urls:
self._enqueue_url(url)
# Layer 2: Try llms.txt (unless explicitly disabled)
if not sitemap_urls 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
# Layer 3: SPA nav discovery (browser mode only, when other layers found few pages)
if self.browser_mode and len(self.pending_urls) <= 1:
spa_urls = self._discover_spa_nav()
for url in spa_urls:
self._enqueue_url(url)
# HTML scraping (sync/thread-based logic)
logger.info("\n" + "=" * 60)
@@ -2158,7 +2308,7 @@ def setup_argument_parser() -> argparse.ArgumentParser:
configs/react.json
"""
parser = argparse.ArgumentParser(
description="Convert documentation websites to Claude skills",
description="Convert documentation websites to AI skills",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -2444,11 +2594,11 @@ def execute_scraping_and_building(
def execute_enhancement(config: dict[str, Any], args: argparse.Namespace, converter=None) -> None:
"""Execute optional SKILL.md enhancement with Claude.
"""Execute optional SKILL.md enhancement with AI.
Supports two enhancement modes:
1. API-based enhancement (requires ANTHROPIC_API_KEY)
2. Local enhancement using Claude Code (no API key needed)
2. Local enhancement using a coding agent CLI (no API key needed)
Prints appropriate messages and suggestions based on whether
enhancement was requested and whether it succeeded.
@@ -2490,10 +2640,11 @@ def execute_enhancement(config: dict[str, Any], args: argparse.Namespace, conver
try:
enhance_cmd = ["skill-seekers-enhance", f"output/{config['name']}/"]
enhance_cmd.extend(["--enhance-level", str(args.enhance_level)])
if args.api_key:
enhance_cmd.extend(["--api-key", args.api_key])
if getattr(args, "agent", None):
enhance_cmd.extend(["--agent", args.agent])
if getattr(args, "interactive_enhancement", False):
enhance_cmd.append("--interactive-enhancement")
@@ -2505,9 +2656,8 @@ def execute_enhancement(config: dict[str, Any], args: argparse.Namespace, conver
except FileNotFoundError:
logger.warning("\n⚠ skill-seekers-enhance command not found. Run manually:")
logger.info(
" skill-seekers-enhance output/%s/ --enhance-level %d",
" skill-seekers enhance output/%s/",
config["name"],
args.enhance_level,
)
# Print packaging instructions
@@ -2516,8 +2666,8 @@ def execute_enhancement(config: dict[str, Any], args: argparse.Namespace, conver
# Suggest enhancement if not done
if getattr(args, "enhance_level", 0) == 0:
logger.info("\n💡 Optional: Enhance SKILL.md with Claude:")
logger.info(" skill-seekers-enhance output/%s/ --enhance-level 2", config["name"])
logger.info("\n💡 Optional: Enhance SKILL.md with AI:")
logger.info(" skill-seekers enhance output/%s/", config["name"])
logger.info(" or re-run with: --enhance-level 2 (auto-detects API vs LOCAL mode)")
logger.info(
" API-based: skill-seekers-enhance-api output/%s/",

View File

@@ -60,7 +60,13 @@ OPTIONAL_DEPS = {
}
# ── API keys to check ────────────────────────────────────────────────────────
API_KEYS = ["ANTHROPIC_API_KEY", "GITHUB_TOKEN", "GOOGLE_API_KEY", "OPENAI_API_KEY"]
API_KEYS = [
"ANTHROPIC_API_KEY",
"GITHUB_TOKEN",
"GOOGLE_API_KEY",
"OPENAI_API_KEY",
"MOONSHOT_API_KEY",
]
def _try_import(module_name: str) -> tuple[bool, str]:

View File

@@ -4,25 +4,28 @@ Smart Enhancement Dispatcher
Routes `skill-seekers enhance` to the correct backend:
API mode — when an API key is available (Claude/Gemini/OpenAI).
API mode — when an API key is available (Anthropic/Gemini/OpenAI).
Calls enhance_skill.py which uses platform adaptors.
LOCAL mode — when no API key is found.
Calls LocalSkillEnhancer from enhance_skill_local.py.
Supports: Claude Code, OpenAI Codex, GitHub Copilot, OpenCode, Kimi, and other agents.
Decision priority:
1. Explicit --target flag → API mode with that platform.
2. Config ai_enhancement.default_agent + matching env key → API mode.
3. Auto-detect from env vars: ANTHROPIC_API_KEY → claude,
GOOGLE_API_KEY → gemini, OPENAI_API_KEY → openai.
4. No API keys → LOCAL mode (Claude Code CLI).
5. LOCAL mode + running as root → clear error (Claude Code refuses root).
4. No API keys → LOCAL mode (AI coding agent).
5. LOCAL mode + running as root → clear error (AI coding agent refuses root).
"""
import os
import sys
from pathlib import Path
from skill_seekers.cli.agent_client import get_default_timeout
# ---------------------------------------------------------------------------
# Helpers
@@ -43,6 +46,7 @@ def _get_api_keys() -> dict[str, str | None]:
"claude": (os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN")),
"gemini": os.environ.get("GOOGLE_API_KEY"),
"openai": os.environ.get("OPENAI_API_KEY"),
"kimi": os.environ.get("MOONSHOT_API_KEY"),
}
@@ -73,11 +77,11 @@ def _pick_mode(args) -> tuple[str, str | None]:
# 2. Config default_agent preference (if a matching key is available).
config_agent = _get_config_default_agent()
if config_agent in ("claude", "gemini", "openai") and api_keys.get(config_agent):
if config_agent in ("claude", "gemini", "openai", "kimi") and api_keys.get(config_agent):
return "api", config_agent
# 3. Auto-detect from environment variables.
# Priority: Claude > Gemini > OpenAI (Claude is Anthropic's native platform).
# Priority: Anthropic > Gemini > OpenAI.
if api_keys["claude"]:
return "api", "claude"
if api_keys["gemini"]:
@@ -156,7 +160,7 @@ def _run_local_mode(args) -> int:
headless = not interactive
success = enhancer.run(
headless=headless,
timeout=getattr(args, "timeout", 600),
timeout=getattr(args, "timeout", None) or get_default_timeout(),
background=getattr(args, "background", False),
daemon=getattr(args, "daemon", False),
)
@@ -176,15 +180,15 @@ def main() -> int:
parser = argparse.ArgumentParser(
description=(
"Enhance SKILL.md using AI. "
"Automatically selects API mode (Gemini/OpenAI/Claude API) when an API key "
"is available, or falls back to LOCAL mode (Claude Code CLI)."
"Automatically selects API mode (Anthropic/Gemini/OpenAI API) when an API key "
"is available, or falls back to LOCAL mode (AI coding agent)."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Mode selection (automatic — no flags required):
API mode : Set ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY.
Or use --target to force a platform.
LOCAL mode: Falls back when no API keys are found. Requires Claude Code CLI.
LOCAL mode: Falls back when no API keys are found. Requires AI coding agent.
Does NOT work as root (Docker/VPS) — use API mode instead.
Examples:
@@ -194,7 +198,7 @@ Examples:
# Force Gemini API
skill-seekers enhance output/react/ --target gemini
# Force Claude API with explicit key
# Force Anthropic API with explicit key
skill-seekers enhance output/react/ --target claude --api-key sk-ant-...
# LOCAL mode options
@@ -244,10 +248,10 @@ Examples:
if _is_root():
print("❌ Cannot run LOCAL enhancement as root.")
print()
print(" Claude Code CLI refuses to execute as root (Docker/VPS security policy).")
print(" AI coding agent refuses to execute as root (Docker/VPS security policy).")
print(" Use API mode instead by setting one of these environment variables:")
print()
print(" export ANTHROPIC_API_KEY=sk-ant-... # Claude")
print(" export ANTHROPIC_API_KEY=sk-ant-... # Anthropic")
print(" export GOOGLE_API_KEY=AIza... # Gemini")
print(" export OPENAI_API_KEY=sk-proj-... # OpenAI")
print()
@@ -255,7 +259,8 @@ Examples:
print(f" skill-seekers enhance {args.skill_directory}")
return 1
print("🤖 Enhancement mode: LOCAL (Claude Code CLI)")
agent_name = os.environ.get("SKILL_SEEKER_AGENT", "claude").strip() or "claude"
print(f"🤖 Enhancement mode: LOCAL ({agent_name})")
return _run_local_mode(args)

View File

@@ -4,7 +4,7 @@ SKILL.md Enhancement Script
Uses platform AI APIs to improve SKILL.md by analyzing reference documentation.
Usage:
# Claude (default)
# Anthropic (default)
skill-seekers enhance output/react/
skill-seekers enhance output/react/ --api-key sk-ant-...
@@ -63,17 +63,17 @@ class SkillEnhancer:
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"""
"""Use AI to enhance SKILL.md"""
# Build prompt
prompt = self._build_enhancement_prompt(references, current_skill_md)
print("\n🤖 Asking Claude to enhance SKILL.md...")
print("\n🤖 Asking AI to enhance SKILL.md...")
print(f" Input: {len(prompt):,} characters")
try:
message = self.client.messages.create(
model="claude-sonnet-4-20250514",
model=os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514"),
max_tokens=4096,
temperature=0.3,
messages=[{"role": "user", "content": prompt}],
@@ -94,7 +94,7 @@ class SkillEnhancer:
return enhanced_content
except Exception as e:
print(f"❌ Error calling Claude API: {e}")
print(f"❌ Error calling AI API: {e}")
return None
def _is_video_source(self, references):
@@ -102,7 +102,7 @@ class SkillEnhancer:
return any(meta["source"] == "video_tutorial" for meta in references.values())
def _build_enhancement_prompt(self, references, current_skill_md):
"""Build the prompt for Claude with multi-source awareness"""
"""Build the prompt for AI with multi-source awareness"""
# Dispatch to video-specific prompt if video source detected
if self._is_video_source(references):
@@ -119,7 +119,7 @@ class SkillEnhancer:
# Analyze conflicts if present
has_conflicts = any("conflicts" in meta["path"] for meta in references.values())
prompt = f"""You are enhancing a Claude skill's SKILL.md file. This skill is about: {skill_name}
prompt = f"""You are enhancing an LLM skill's SKILL.md file. This skill is about: {skill_name}
I've scraped documentation from multiple sources and organized it into reference files. Your job is to create an EXCELLENT SKILL.md that synthesizes knowledge from these sources.
@@ -275,7 +275,7 @@ IMPORTANT:
- 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
- Maintain the markdown structure for LLM skills
- Keep code examples properly formatted with language tags
OUTPUT:
@@ -294,7 +294,7 @@ Return ONLY the complete SKILL.md content, starting with the frontmatter (---).
"""
skill_name = self.skill_dir.name
prompt = f"""You are enhancing a Claude skill built from VIDEO TUTORIAL extraction. This skill is about: {skill_name}
prompt = f"""You are enhancing an LLM skill built from VIDEO TUTORIAL extraction. This skill is about: {skill_name}
The raw data was extracted from video tutorials using:
1. **Transcript** (speech-to-text) — HIGH quality, this is the primary signal
@@ -471,7 +471,7 @@ Return ONLY the complete SKILL.md content, starting with the frontmatter (---).
else:
print(" No existing SKILL.md, will create new one")
# Enhance with Claude
# Enhance with AI
enhanced = self.enhance_skill_md(references, current_skill_md)
if not enhanced:
@@ -502,7 +502,7 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Claude (default)
# Anthropic (default)
export ANTHROPIC_API_KEY=sk-ant-...
skill-seekers enhance output/react/
@@ -530,9 +530,9 @@ Examples:
)
parser.add_argument(
"--target",
choices=["claude", "gemini", "openai"],
default="claude",
help="Target LLM platform (default: claude)",
choices=["claude", "gemini", "openai", "kimi"],
default=None,
help="Target LLM platform (auto-detected from API keys, or 'claude' if none set)",
)
parser.add_argument(
"--dry-run", action="store_true", help="Show what would be done without calling API"
@@ -540,6 +540,12 @@ Examples:
args = parser.parse_args()
# Auto-detect target platform if not specified
if args.target is None:
from skill_seekers.cli.agent_client import AgentClient
args.target = AgentClient.detect_default_target()
# Validate skill directory
skill_dir = Path(args.skill_dir)
if not skill_dir.exists():
@@ -578,7 +584,7 @@ Examples:
if not adaptor.supports_enhancement():
print(f"❌ Error: {adaptor.PLATFORM_NAME} does not support AI enhancement")
print("\nSupported platforms for enhancement:")
print(" - Claude AI (Anthropic)")
print(" - Anthropic (Claude AI)")
print(" - Google Gemini")
print(" - OpenAI ChatGPT")
sys.exit(1)

View File

@@ -132,6 +132,12 @@ AGENT_PRESETS = {
"command": ["opencode"],
"supports_skip_permissions": False,
},
"kimi": {
"display_name": "Kimi Code CLI",
"command": ["kimi", "--print", "--input-format", "text", "--work-dir", "{skill_dir}"],
"supports_skip_permissions": False,
"uses_stdin": True,
},
}
@@ -146,6 +152,7 @@ def _normalize_agent_name(agent_name: str) -> str:
"copilot-cli": "copilot",
"open-code": "opencode",
"open_code": "opencode",
"kimi-cli": "kimi",
}
return aliases.get(normalized, normalized)
@@ -238,6 +245,8 @@ class LocalSkillEnhancer:
if "{prompt_file}" in arg:
cmd_parts[idx] = arg.replace("{prompt_file}", prompt_file)
uses_prompt_file = True
if "{skill_dir}" in arg:
cmd_parts[idx] = arg.replace("{skill_dir}", str(self.skill_dir.resolve()))
return cmd_parts, uses_prompt_file
@@ -652,7 +661,7 @@ After writing, the file SKILL.md should:
except Exception:
return None
def run(self, headless=True, timeout=600, background=False, daemon=False):
def run(self, headless=True, timeout=2700, background=False, daemon=False):
"""Main enhancement workflow with automatic smart summarization for large skills.
Automatically detects large skills (>30K chars) and applies smart summarization
@@ -666,7 +675,7 @@ After writing, the file SKILL.md should:
Args:
headless: If True, run local agent directly without opening terminal (default: True)
timeout: Maximum time to wait for enhancement in seconds (default: 600 = 10 minutes)
timeout: Maximum time to wait for enhancement in seconds (default: 2700 = 45 minutes)
background: If True, run in background and return immediately (default: False)
daemon: If True, run as persistent daemon with monitoring (default: False)
@@ -711,7 +720,7 @@ After writing, the file SKILL.md should:
print("⚠️ LARGE SKILL DETECTED")
print(f" 📊 Reference content: {total_size:,} characters")
if self.agent == "claude":
print(" 💡 Claude CLI limit: ~30,000-40,000 characters")
print(" 💡 CLI agent limit: ~30,000-40,000 characters")
else:
print(" 💡 Local CLI agents often have input limits; summarizing to be safe")
print()
@@ -739,7 +748,7 @@ After writing, the file SKILL.md should:
if use_summarization:
print(f" ✓ Prompt created and optimized ({len(prompt):,} characters)")
if self.agent == "claude":
print(" ✓ Ready for Claude CLI (within safe limits)")
print(" ✓ Ready for CLI agent (within safe limits)")
else:
print(" ✓ Ready for local CLI (within safe limits)")
print()
@@ -905,6 +914,45 @@ rm {prompt_file}
print("❌ SKILL.md not found after enhancement")
return False
else:
# Exit code 75 = EX_TEMPFAIL (retryable temporary failure from Kimi CLI)
if result.returncode == 75:
print(f"⚠️ {self.agent_display} returned temporary failure (exit code: 75)")
print(
" This usually means a transient API issue (timeout, rate limit, or empty response)."
)
print(" Retrying once in 5 seconds...")
print()
time.sleep(5)
result_retry, error = self._run_agent_command(
prompt_file, timeout, include_permissions_flag=True
)
if error:
print(f"{error}")
with contextlib.suppress(Exception):
os.unlink(prompt_file)
return False
if result_retry.returncode == 0:
elapsed = time.time() - start_time
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 on retry! ({elapsed:.1f} seconds)")
print(f" SKILL.md updated: {new_size:,} bytes")
print()
with contextlib.suppress(Exception):
os.unlink(prompt_file)
return True
print(f"❌ Retry also failed (exit code: {result_retry.returncode})")
if result_retry.stderr:
stderr_lines = result_retry.stderr.strip().split("\n")
for line in stderr_lines[:10]:
print(f" | {line}")
print(" Try again later or use API mode:")
print(" export ANTHROPIC_API_KEY=sk-ant-...")
print(f" skill-seekers enhance {self.skill_dir} --target claude")
return False
print(f"{self.agent_display} returned error (exit code: {result.returncode})")
if result.stderr:
stderr_lines = result.stderr.strip().split("\n")
@@ -919,7 +967,7 @@ rm {prompt_file}
):
print()
print(" ⚠️ This looks like a root/permission error.")
print(" Claude Code CLI refuses to run as root (security policy).")
print(" The CLI agent refuses to run as root (security policy).")
print(" Use API mode instead:")
print(" export ANTHROPIC_API_KEY=sk-ant-...")
print(f" skill-seekers enhance {self.skill_dir} --target claude")
@@ -1289,10 +1337,10 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Auto-detection (no flags needed):
If ANTHROPIC_API_KEY is set → Claude API mode
If ANTHROPIC_API_KEY is set → Anthropic API mode
If GOOGLE_API_KEY is set → Gemini API mode
If OPENAI_API_KEY is set → OpenAI API mode
Otherwise → LOCAL mode (Claude Code Max, free)
Otherwise → LOCAL mode (coding agent CLI, free)
Examples:
# Auto-detect mode based on env vars (recommended)
@@ -1373,11 +1421,16 @@ Force Mode (LOCAL only, Default ON):
help="Disable force mode: enable confirmation prompts (default: force mode ON)",
)
from skill_seekers.cli.agent_client import get_default_timeout
parser.add_argument(
"--timeout",
type=int,
default=600,
help="Timeout in seconds for headless mode (default: 600 = 10 minutes)",
default=get_default_timeout(),
help=(
"Timeout in seconds for headless mode "
"(default: 45 minutes, set SKILL_SEEKER_ENHANCE_TIMEOUT to override)"
),
)
args = parser.parse_args()

View File

@@ -84,12 +84,13 @@ class WorkflowEngine:
- Target specific parts of the analysis
"""
def __init__(self, workflow: EnhancementWorkflow | str | Path):
def __init__(self, workflow: EnhancementWorkflow | str | Path, agent: str | None = None):
"""
Initialize workflow engine.
Args:
workflow: EnhancementWorkflow object or path to YAML file
agent: Local CLI agent name (e.g., "kimi", "claude")
"""
if isinstance(workflow, (str, Path)):
self.workflow = self._load_workflow(workflow)
@@ -98,6 +99,7 @@ class WorkflowEngine:
self.history: list[dict[str, Any]] = []
self.enhancer = None # Lazy load UnifiedEnhancer
self.agent = agent
def _load_workflow(self, workflow_ref: str | Path) -> EnhancementWorkflow:
"""Load workflow from YAML file using 3-level search order.
@@ -350,15 +352,23 @@ class WorkflowEngine:
current = context["current_results"]
# Determine what to enhance based on target
if stage.target == "patterns" and "patterns" in current:
enhancer = PatternEnhancer()
enhanced_patterns = enhancer.enhance_patterns(current["patterns"])
return {"patterns": enhanced_patterns}
if stage.target == "patterns":
if "patterns" in current:
enhancer = PatternEnhancer(agent=self.agent)
enhanced_patterns = enhancer.enhance_patterns(current["patterns"])
return {"patterns": enhanced_patterns}
else:
logger.info(f" No {stage.target} data available, skipping builtin stage")
return {}
elif stage.target == "examples" and "examples" in current:
enhancer = TestExampleEnhancer()
enhanced_examples = enhancer.enhance_examples(current["examples"])
return {"examples": enhanced_examples}
elif stage.target == "examples":
if "examples" in current:
enhancer = TestExampleEnhancer(agent=self.agent)
enhanced_examples = enhancer.enhance_examples(current["examples"])
return {"examples": enhanced_examples}
else:
logger.info(f" No {stage.target} data available, skipping builtin stage")
return {}
else:
logger.warning(f"Unknown builtin target: {stage.target}")
@@ -374,7 +384,7 @@ class WorkflowEngine:
if not self.enhancer:
from skill_seekers.cli.ai_enhancer import AIEnhancer
self.enhancer = AIEnhancer()
self.enhancer = AIEnhancer(agent=self.agent)
# Format prompt with context
try:
@@ -385,7 +395,11 @@ class WorkflowEngine:
# Call AI with custom prompt
logger.info(f" 🤖 Running custom AI prompt...")
response = self.enhancer._call_claude(formatted_prompt, max_tokens=3000)
# Use call() (agent-agnostic) with _call_claude() as fallback for older enhancers
if hasattr(self.enhancer, "call"):
response = self.enhancer.call(formatted_prompt, max_tokens=3000)
else:
response = self.enhancer._call_claude(formatted_prompt, max_tokens=3000)
if not response:
logger.warning(f" ⚠️ No response from AI")

View File

@@ -69,7 +69,7 @@ def infer_description_from_epub(metadata: dict | None = None, name: str = "") ->
class EpubToSkillConverter:
"""Convert EPUB e-book to Claude skill."""
"""Convert EPUB e-book to AI skill."""
def __init__(self, config):
self.config = config
@@ -1179,13 +1179,17 @@ def main():
print("❌ API enhancement not available. Falling back to LOCAL mode...")
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
else:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")

View File

@@ -1202,7 +1202,7 @@ Examples:
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("4. Upload router + all sub-skills to target platform")
print("")

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""
GitHub Repository to Claude Skill Converter (Tasks C1.1-C1.12)
GitHub Repository to AI Skill Converter (Tasks C1.1-C1.12)
Converts GitHub repositories into Claude AI skills by extracting:
Converts GitHub repositories into AI skills by extracting:
- README and documentation
- Code structure and signatures
- GitHub Issues, Changelog, and Releases
@@ -714,17 +714,7 @@ class GitHubScraper:
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
# Build reverse extension → language map for per-file detection
extension_map = {
"Python": [".py"],
"JavaScript": [".js", ".jsx"],
@@ -733,72 +723,86 @@ class GitHubScraper:
"Java": [".java"],
"C": [".c", ".h"],
"C++": [".cpp", ".hpp", ".cc", ".hh", ".cxx"],
"C#": [".cs"],
"Go": [".go"],
"Rust": [".rs"],
"Swift": [".swift"],
"Ruby": [".rb"],
"PHP": [".php"],
"GDScript": [".gd"],
}
ext_to_lang = {}
for lang, exts in extension_map.items():
for ext in exts:
ext_to_lang[ext] = lang
extensions = extension_map.get(primary_language, [])
if not extensions:
logger.warning(f"No file extensions mapped for {primary_language}")
return
# Optional: filter to specific languages from config
target_languages = None
config_language = self.config.get("language", "")
if config_language:
target_languages = {lang.strip() for lang in config_language.split(",")}
logger.info(f"Language filter from config: {', '.join(sorted(target_languages))}")
# Analyze files matching patterns and extensions
# Analyze ALL files, detecting language per-file from extension
analyzed_files = []
file_tree = self.extracted_data.get("file_tree", [])
languages_found = set()
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):
# Detect language from file extension
ext = os.path.splitext(file_path)[1].lower()
language = ext_to_lang.get(ext)
if not language:
continue
# Check if file matches patterns (if specified)
# Apply language filter if config specifies target languages
if target_languages and language not in target_languages:
continue
# Check if file matches patterns (if specified in config)
if self.file_patterns and not any(
fnmatch.fnmatch(file_path, pattern) for pattern in self.file_patterns
):
continue
# Analyze this file
# Analyze this file with the correct language
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, 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
)
analysis_result = self.code_analyzer.analyze_file(file_path, content, 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"
{"file": file_path, "language": language, **analysis_result}
)
languages_found.add(language)
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("Reached analysis limit (50 files, GitHub API mode)")
break
# Determine primary language for backward compat in output
repo_languages = self.extracted_data.get("languages", {})
primary_language = (
max(repo_languages.items(), key=lambda x: x[1]["bytes"])[0]
if repo_languages
else "Unknown"
)
self.extracted_data["code_analysis"] = {
"depth": self.code_analysis_depth,
"language": primary_language,
"languages_analyzed": sorted(languages_found),
"files_analyzed": len(analyzed_files),
"files": analyzed_files,
}
@@ -807,8 +811,9 @@ class GitHubScraper:
total_classes = sum(len(f.get("classes", [])) for f in analyzed_files)
total_functions = sum(len(f.get("functions", [])) for f in analyzed_files)
lang_summary = ", ".join(sorted(languages_found)) if languages_found else "none"
logger.info(
f"Code analysis complete: {len(analyzed_files)} files, {total_classes} classes, {total_functions} functions"
f"Code analysis complete: {len(analyzed_files)} files, {total_classes} classes, {total_functions} functions ({lang_summary})"
)
def _extract_issues(self):
@@ -913,7 +918,7 @@ class GitHubScraper:
class GitHubToSkillConverter:
"""
Convert extracted GitHub data to Claude skill format (C1.10).
Convert extracted GitHub data to AI skill format (C1.10).
"""
def __init__(self, config: dict[str, Any]):
@@ -1387,7 +1392,7 @@ def setup_argument_parser() -> argparse.ArgumentParser:
argparse.ArgumentParser: Configured argument parser
"""
parser = argparse.ArgumentParser(
description="GitHub Repository to Claude Skill Converter",
description="GitHub Repository to AI Skill Converter",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
@@ -1518,23 +1523,29 @@ def main():
from pathlib import Path
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
logger.info("✅ Local enhancement complete!")
agent_name = agent or "claude"
logger.info(f"✅ Local enhancement complete! (via {agent_name})")
else:
# LOCAL enhancement (no API key)
from pathlib import Path
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
logger.info("✅ Local enhancement complete!")
agent_name = agent or "claude"
logger.info(f"✅ Local enhancement complete! (via {agent_name})")
logger.info(f"\n✅ Success! Skill created at: {skill_dir}/")
# Only suggest enhancement if neither workflow nor traditional enhancement was done
if not workflow_executed and getattr(args, "enhance_level", 0) == 0:
logger.info("\n💡 Optional: Enhance SKILL.md with Claude:")
logger.info("\n💡 Optional: Enhance SKILL.md with AI:")
logger.info(f" skill-seekers enhance {skill_dir}/ --enhance-level 2")
logger.info(" (auto-detects API vs LOCAL mode based on ANTHROPIC_API_KEY)")
logger.info("\n💡 Or use a workflow:")

View File

@@ -2,8 +2,8 @@
AI Enhancement for How-To Guides (C3.3)
This module provides comprehensive AI enhancement for how-to guides with dual-mode support:
- API mode: Uses Claude API (requires ANTHROPIC_API_KEY)
- LOCAL mode: Uses Claude Code CLI (no API key needed)
- API mode: Uses Anthropic API (requires ANTHROPIC_API_KEY)
- LOCAL mode: Uses a coding agent CLI (no API key needed)
Provides 5 automatic enhancements:
1. Step Descriptions - Natural language explanations (not just syntax)
@@ -15,11 +15,7 @@ Provides 5 automatic enhancements:
import json
import logging
import os
import subprocess
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING
# Avoid circular imports by using TYPE_CHECKING
@@ -47,14 +43,8 @@ else:
logger = logging.getLogger(__name__)
# Conditional import for Anthropic API
try:
import anthropic
ANTHROPIC_AVAILABLE = True
except ImportError:
ANTHROPIC_AVAILABLE = False
logger.debug("Anthropic library not available - API mode will be unavailable")
# ANTHROPIC_AVAILABLE kept for backward compatibility — AgentClient handles detection
ANTHROPIC_AVAILABLE = True # Detection delegated to AgentClient
@dataclass
@@ -71,8 +61,8 @@ class GuideEnhancer:
AI enhancement for how-to guides with dual-mode support.
Modes:
- api: Uses Claude API (requires ANTHROPIC_API_KEY)
- local: Uses Claude Code CLI (no API key needed)
- api: Uses Anthropic API (requires ANTHROPIC_API_KEY)
- local: Uses a coding agent CLI (no API key needed)
- auto: Automatically detect best mode
"""
@@ -83,77 +73,17 @@ class GuideEnhancer:
Args:
mode: Enhancement mode - "api", "local", or "auto"
"""
self.mode = self._detect_mode(mode)
self.api_key = os.environ.get("ANTHROPIC_API_KEY")
self.client = None
from skill_seekers.cli.agent_client import AgentClient
if self.mode == "api":
if ANTHROPIC_AVAILABLE and self.api_key:
# Support custom base_url for GLM-4.7 and other Claude-compatible APIs
client_kwargs = {"api_key": self.api_key}
base_url = os.environ.get("ANTHROPIC_BASE_URL")
if base_url:
client_kwargs["base_url"] = base_url
logger.info(f"✅ Using custom API base URL: {base_url}")
self.client = anthropic.Anthropic(**client_kwargs)
logger.info("✨ GuideEnhancer initialized in API mode")
else:
logger.warning(
"⚠️ API mode requested but anthropic library not available or no API key"
)
self.mode = "none"
elif self.mode == "local":
# Check if claude CLI is available
if not self._check_claude_cli():
logger.warning("⚠️ Claude CLI not found - falling back to API mode")
self.mode = "api"
if ANTHROPIC_AVAILABLE and self.api_key:
# Support custom base_url for GLM-4.7 and other Claude-compatible APIs
client_kwargs = {"api_key": self.api_key}
base_url = os.environ.get("ANTHROPIC_BASE_URL")
if base_url:
client_kwargs["base_url"] = base_url
logger.info(f"✅ Using custom API base URL: {base_url}")
self.client = anthropic.Anthropic(**client_kwargs)
else:
logger.warning("⚠️ API fallback also unavailable")
self.mode = "none"
else:
logger.info("✨ GuideEnhancer initialized in LOCAL mode")
self._agent = AgentClient(mode=mode)
self.mode = self._agent.mode
if self._agent.is_available():
self._agent.log_mode()
else:
logger.warning("⚠️ No AI enhancement available (no API key or Claude CLI)")
logger.warning("⚠️ No AI enhancement available")
self.mode = "none"
def _detect_mode(self, requested_mode: str) -> str:
"""
Detect the best enhancement mode.
Args:
requested_mode: User-requested mode
Returns:
Detected mode: "api", "local", or "none"
"""
if requested_mode == "auto":
# Prefer API if key available, else LOCAL
if os.environ.get("ANTHROPIC_API_KEY") and ANTHROPIC_AVAILABLE:
return "api"
elif self._check_claude_cli():
return "local"
else:
return "none"
return requested_mode
def _check_claude_cli(self) -> bool:
"""Check if Claude Code CLI is available."""
try:
result = subprocess.run(
["claude", "--version"], capture_output=True, text=True, timeout=5
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def enhance_guide(self, guide_data: dict) -> dict:
"""
Apply all 5 enhancements to a guide.
@@ -332,7 +262,7 @@ class GuideEnhancer:
def _call_ai(self, prompt: str, max_tokens: int = 4000) -> str | None:
"""
Call AI with the given prompt.
Call AI with the given prompt via AgentClient.
Args:
prompt: Prompt text
@@ -341,73 +271,7 @@ class GuideEnhancer:
Returns:
AI response text or None if failed
"""
if self.mode == "api":
return self._call_claude_api(prompt, max_tokens)
elif self.mode == "local":
return self._call_claude_local(prompt)
return None
def _call_claude_api(self, prompt: str, max_tokens: int = 4000) -> str | None:
"""
Call Claude API.
Args:
prompt: Prompt text
max_tokens: Maximum tokens in response
Returns:
API response text or None if failed
"""
if not self.client:
return None
try:
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
except Exception as e:
logger.warning(f"⚠️ Claude API call failed: {e}")
return None
def _call_claude_local(self, prompt: str) -> str | None:
"""
Call Claude Code CLI.
Args:
prompt: Prompt text
Returns:
CLI response text or None if failed
"""
try:
# Create temporary prompt file
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write(prompt)
prompt_file = f.name
# Run claude CLI
result = subprocess.run(
["claude", prompt_file],
capture_output=True,
text=True,
timeout=300, # 5 min timeout
)
# Clean up prompt file
Path(prompt_file).unlink(missing_ok=True)
if result.returncode == 0:
return result.stdout
else:
logger.warning(f"⚠️ Claude CLI failed: {result.stderr}")
return None
except (subprocess.TimeoutExpired, Exception) as e:
logger.warning(f"⚠️ Claude CLI execution failed: {e}")
return None
return self._agent.call(prompt, max_tokens=max_tokens)
# === Prompt Creation Methods ===
@@ -422,7 +286,7 @@ class GuideEnhancer:
Enhanced guide data
"""
prompt = self._create_enhancement_prompt(guide_data)
response = self._call_claude_api(prompt)
response = self._call_ai(prompt)
if not response:
return guide_data

View File

@@ -1785,7 +1785,7 @@ def main() -> int:
"0=disabled (default for HTML), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"otherwise LOCAL (Claude Code, Kimi, etc.)"
)
# HTML-specific args
@@ -1907,7 +1907,9 @@ def main() -> int:
LocalSkillEnhancer,
)
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
else:
@@ -1915,7 +1917,9 @@ def main() -> int:
LocalSkillEnhancer,
)
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")

View File

@@ -301,6 +301,8 @@ def install_to_agent(
msg += "Restart Cursor to load the new skill."
elif agent_name.lower() in ["vscode", "copilot"]:
msg += "Restart VS Code to load the new skill."
elif agent_name.lower() == "kimi-code":
msg += "Restart Kimi Code to load the new skill."
else:
msg += f"Restart {agent_name.capitalize()} to load the new skill."

View File

@@ -74,7 +74,7 @@ Examples:
# Preview workflow (dry run)
skill-seekers install --config react --dry-run
# Install for Gemini instead of Claude
# Install for Gemini instead of default platform
skill-seekers install --config react --target gemini
# Install for OpenAI ChatGPT
@@ -107,7 +107,9 @@ Phases:
help="Output directory for skill files (default: output/)",
)
parser.add_argument("--no-upload", action="store_true", help="Skip automatic upload to Claude")
parser.add_argument(
"--no-upload", action="store_true", help="Skip automatic upload to target platform"
)
parser.add_argument(
"--unlimited",
@@ -119,13 +121,19 @@ Phases:
parser.add_argument(
"--target",
choices=["claude", "gemini", "openai", "markdown"],
default="claude",
help="Target LLM platform (default: claude)",
choices=["claude", "gemini", "openai", "kimi", "markdown"],
default=None,
help="Target LLM platform (auto-detected from API keys, or 'claude' if none set)",
)
args = parser.parse_args()
# Auto-detect target platform if not specified
if args.target is None:
from skill_seekers.cli.agent_client import AgentClient
args.target = AgentClient.detect_default_target()
# Determine if config is a name or path
config_arg = args.config
if config_arg.endswith(".json") or "/" in config_arg or "\\" in config_arg:

View File

@@ -1182,13 +1182,17 @@ def main() -> int:
print("❌ API enhancement not available. Falling back to LOCAL mode...")
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
else:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")

View File

@@ -30,7 +30,7 @@ Commands:
enhance AI-powered enhancement (auto: API or LOCAL mode)
enhance-status Check enhancement status (for background/daemon modes)
package Package skill into .zip file
upload Upload skill to Claude
upload Upload skill to target platform
estimate Estimate page count before scraping
extract-test-examples Extract usage examples from test files
install-agent Install skill to AI agent directories
@@ -47,6 +47,7 @@ Examples:
import argparse
import importlib
import os
import sys
from pathlib import Path
@@ -101,7 +102,7 @@ def create_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="skill-seekers",
description="Convert documentation, GitHub repos, and PDFs into Claude AI skills",
description="Convert documentation, GitHub repos, and PDFs into AI skills",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
@@ -391,7 +392,7 @@ def _handle_analyze_command(args: argparse.Namespace) -> int:
background=False,
daemon=False,
no_force=False,
timeout=600,
timeout=2700,
)
_mode, _target = _pick_mode(_fake_args)
@@ -403,7 +404,10 @@ def _handle_analyze_command(args: argparse.Namespace) -> int:
print(" Set ANTHROPIC_API_KEY / GOOGLE_API_KEY to enable API mode")
success = False
else:
print("\n🤖 Enhancement mode: LOCAL (Claude Code CLI)")
agent_name = (
os.environ.get("SKILL_SEEKER_AGENT", "claude").strip() or "claude"
)
print(f"\n🤖 Enhancement mode: LOCAL ({agent_name})")
success = _run_local_mode(_fake_args) == 0
if success:

View File

@@ -1332,7 +1332,7 @@ def main() -> int:
"0=disabled (default for man), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"otherwise LOCAL (Claude Code, Kimi, etc.)"
)
# Man-specific arguments
@@ -1486,13 +1486,17 @@ def main() -> int:
print("❌ API enhancement not available. Falling back to LOCAL mode...")
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
else:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")

View File

@@ -4,7 +4,7 @@ Source Merger for Multi-Source Skills
Merges documentation and code data intelligently with GitHub insights:
- Rule-based merge: Fast, deterministic rules
- Claude-enhanced merge: AI-powered reconciliation
- AI-enhanced merge: AI-powered reconciliation
Handles conflicts and creates unified API reference with GitHub metadata.
@@ -18,7 +18,6 @@ Multi-layer architecture (Phase 3):
import json
import logging
import os
import subprocess
import tempfile
from typing import Any, Optional
@@ -440,11 +439,11 @@ class RuleBasedMerger:
return signature
class ClaudeEnhancedMerger:
class AIEnhancedMerger:
"""
Claude-enhanced API merger using local Claude Code with GitHub insights.
AI-enhanced API merger using local AI coding agent with GitHub insights.
Opens Claude Code in a new terminal to intelligently reconcile conflicts.
Uses the configured AI agent to intelligently reconcile conflicts.
Uses the same approach as enhance_skill_local.py.
Multi-layer architecture (Phase 3):
@@ -462,7 +461,7 @@ class ClaudeEnhancedMerger:
github_streams: Optional["ThreeStreamData"] = None,
):
"""
Initialize Claude-enhanced merger with GitHub streams support.
Initialize AI-enhanced merger with GitHub streams support.
Args:
docs_data: Documentation scraper data (Layer 2: HTML docs)
@@ -480,31 +479,31 @@ class ClaudeEnhancedMerger:
def merge_all(self) -> dict[str, Any]:
"""
Merge all APIs using Claude enhancement.
Merge all APIs using AI enhancement.
Returns:
Dict containing merged API data
"""
logger.info("Starting Claude-enhanced merge...")
logger.info("Starting AI-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")
# Launch AI agent for enhancement
logger.info("Launching AI agent for intelligent merging...")
logger.info("AI will analyze conflicts and create reconciled API reference")
try:
self._launch_claude_merge(workspace_dir)
self._launch_ai_merge(workspace_dir)
# Read enhanced results
merged_data = self._read_merged_results(workspace_dir)
logger.info("Claude-enhanced merge complete")
logger.info("AI-enhanced merge complete")
return merged_data
except Exception as e:
logger.error(f"Claude enhancement failed: {e}")
logger.error(f"AI enhancement failed: {e}")
logger.info("Falling back to rule-based merge")
return self.rule_merger.merge_all()
@@ -518,13 +517,13 @@ class ClaudeEnhancedMerger:
workspace = tempfile.mkdtemp(prefix="skill_merge_")
logger.info(f"Created merge workspace: {workspace}")
# Write context files for Claude
# Write context files for AI agent
self._write_context_files(workspace)
return workspace
def _write_context_files(self, workspace: str):
"""Write context files for Claude to analyze."""
"""Write context files for AI agent to analyze."""
# 1. Write conflicts summary
conflicts_file = os.path.join(workspace, "conflicts.json")
@@ -553,7 +552,7 @@ class ClaudeEnhancedMerger:
with open(code_apis_file, "w") as f:
json.dump(detector.code_apis, f, indent=2)
# 4. Write merge instructions for Claude
# 4. Write merge instructions for AI agent
instructions = """# API Merge Task
You are merging API documentation from two sources:
@@ -625,75 +624,79 @@ Take your time to analyze each conflict carefully. The goal is to create the mos
counts[value] = counts.get(value, 0) + 1
return counts
def _launch_claude_merge(self, workspace: str):
def _launch_ai_merge(self, workspace: str):
"""
Launch Claude Code to perform merge.
Similar to enhance_skill_local.py approach.
Run AI-enhanced merge via AgentClient (automated, no terminal).
"""
# Create a script that Claude will execute
script_path = os.path.join(workspace, "merge_script.sh")
from skill_seekers.cli.agent_client import AgentClient
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
# Read context files to build prompt
conflicts_file = os.path.join(workspace, "conflicts.json")
docs_apis_file = os.path.join(workspace, "docs_apis.json")
code_apis_file = os.path.join(workspace, "code_apis.json")
instructions_file = os.path.join(workspace, "MERGE_INSTRUCTIONS.md")
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
with open(instructions_file) as f:
instructions = f.read()
timeout = 3600 # 1 hour max
elapsed = 0
while not os.path.exists(merged_file) and elapsed < timeout:
time.sleep(5)
elapsed += 5
with open(conflicts_file) as f:
conflicts_data = f.read()
if not os.path.exists(merged_file):
raise TimeoutError("Claude merge timed out after 1 hour")
# Limit conflict data to avoid token overflow
if len(conflicts_data) > 30000:
conflicts_data = conflicts_data[:30000] + "\n... (truncated)"
with open(docs_apis_file) as f:
docs_apis = f.read()
if len(docs_apis) > 15000:
docs_apis = docs_apis[:15000] + "\n... (truncated)"
with open(code_apis_file) as f:
code_apis = f.read()
if len(code_apis) > 15000:
code_apis = code_apis[:15000] + "\n... (truncated)"
prompt = f"""{instructions}
## Conflicts Data:
{conflicts_data}
## Documentation APIs:
{docs_apis}
## Code APIs:
{code_apis}
Write the merged_apis.json output as valid JSON following the format in the instructions above.
Return ONLY the JSON, no explanation."""
client = AgentClient(mode="auto")
logger.info(f"Running AI merge via {client.agent_display}...")
response = client.call(prompt, max_tokens=8192)
if response:
# Try to extract JSON from response
import re
json_match = re.search(r"\{[\s\S]*\}", response)
if json_match:
try:
merged = json.loads(json_match.group())
with open(merged_file, "w") as f:
json.dump(merged, f, indent=2)
logger.info("✅ AI merge complete — merged_apis.json written")
return
except json.JSONDecodeError:
logger.warning("⚠️ Could not parse JSON from AI response")
# Fallback: write raw response
with open(merged_file, "w") as f:
f.write(response)
logger.info("✅ AI merge complete (raw response saved)")
else:
raise RuntimeError("AI agent returned no response for merge")
def _read_merged_results(self, workspace: str) -> dict[str, Any]:
"""Read merged results from workspace."""
@@ -804,3 +807,7 @@ if __name__ == "__main__":
print(f" Code only: {summary.get('code_only', 0)}")
print(f" Conflicts: {summary.get('conflict', 0)}")
print(f"\n📄 Saved to: {args.output}")
# Backward compatibility alias
ClaudeEnhancedMerger = AIEnhancedMerger

View File

@@ -1002,11 +1002,17 @@ def main() -> int:
except ImportError:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
LocalSkillEnhancer(Path(skill_dir)).run(headless=True)
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
else:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
LocalSkillEnhancer(Path(skill_dir)).run(headless=True)
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
except RuntimeError as e:
print(f"\n Error: {e}", file=sys.stderr)
sys.exit(1) # noqa: E702

View File

@@ -1817,7 +1817,7 @@ Examples:
"0=disabled (default for OpenAPI), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"otherwise LOCAL (Claude Code, Kimi, etc.)"
)
# OpenAPI-specific arguments
@@ -1932,13 +1932,17 @@ Examples:
print(" API enhancement not available. Falling back to LOCAL mode...")
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print(" Local enhancement complete!")
else:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print(" Local enhancement complete!")

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Simple Skill Packager
Packages a skill directory into a .zip file for Claude.
Packages a skill directory into a .zip file for LLM platforms.
Usage:
skill-seekers package output/steam-inventory/
@@ -197,7 +197,7 @@ def main():
from skill_seekers.cli.arguments.package import add_package_arguments
parser = argparse.ArgumentParser(
description="Package a skill directory into a .zip file for Claude",
description="Package a skill directory into a .zip file for LLM platforms",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
@@ -210,7 +210,7 @@ Examples:
# Skip quality checks (faster, but not recommended)
skill-seekers package output/react/ --skip-quality-check
# Package and auto-upload to Claude
# Package and auto-upload to target platform
skill-seekers package output/react/ --upload
# Get help
@@ -295,6 +295,31 @@ Examples:
print(f"\n❌ Upload error: {e}")
sys.exit(1)
# Publish to marketplace if requested
marketplace_name = getattr(args, "marketplace", None)
if marketplace_name:
try:
from skill_seekers.mcp.marketplace_publisher import MarketplacePublisher
publisher = MarketplacePublisher()
pub_result = publisher.publish(
skill_dir=args.skill_directory,
marketplace_name=marketplace_name,
category=getattr(args, "marketplace_category", "development"),
create_branch=getattr(args, "create_branch", False),
force=True,
)
if pub_result["success"]:
print(f"\n{pub_result['message']}")
print(f" Plugin: {pub_result['plugin_path']}")
print(f" Branch: {pub_result['branch']}")
print(f" Commit: {pub_result['commit_sha']}")
else:
print(f"\n⚠️ Marketplace publish failed: {pub_result['message']}")
except Exception as e:
print(f"\n⚠️ Marketplace publish failed: {e}")
print(" Packaging was successful — publish manually later.")
sys.exit(0)

View File

@@ -23,8 +23,8 @@ class EnhanceParser(SubcommandParser):
def description(self) -> str:
return (
"Enhance SKILL.md using AI. "
"Automatically uses API mode (Gemini/OpenAI/Claude) when an API key is "
"available, or falls back to LOCAL mode (Claude Code CLI)."
"Automatically uses API mode (Anthropic/Gemini/OpenAI) when an API key is "
"available, or falls back to LOCAL mode (coding agent CLI)."
)
def add_arguments(self, parser):

View File

@@ -24,7 +24,7 @@ class InstallAgentParser(SubcommandParser):
parser.add_argument(
"--agent",
required=True,
help="Agent name (claude, cursor, vscode, amp, goose, opencode, all)",
help="Agent name (claude, cursor, vscode, amp, goose, opencode, kimi-code, all)",
)
parser.add_argument(
"--force", action="store_true", help="Overwrite existing installation without asking"

View File

@@ -29,7 +29,7 @@ class InstallParser(SubcommandParser):
"--destination", default="output", help="Output directory (default: output/)"
)
parser.add_argument(
"--no-upload", action="store_true", help="Skip automatic upload to Claude"
"--no-upload", action="store_true", help="Skip automatic upload to target platform"
)
parser.add_argument(
"--unlimited", action="store_true", help="Remove page limits during scraping"

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""
PDF Documentation to Claude Skill Converter (Task B1.6)
PDF Documentation to AI Skill Converter (Task B1.6)
Converts PDF documentation into Claude AI skills.
Converts PDF documentation into AI skills.
Uses pdf_extractor_poc.py for extraction, builds skill structure.
Usage:
@@ -63,7 +63,7 @@ def infer_description_from_pdf(pdf_metadata: dict = None, name: str = "") -> str
class PDFToSkillConverter:
"""Convert PDF documentation to Claude skill"""
"""Convert PDF documentation to AI skill"""
def __init__(self, config):
self.config = config
@@ -637,7 +637,7 @@ def main():
from .arguments.pdf import add_pdf_arguments
parser = argparse.ArgumentParser(
description="Convert PDF documentation to Claude skill",
description="Convert PDF documentation to AI skill",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -749,16 +749,22 @@ def main():
from pathlib import Path
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
agent_name = agent or "claude"
print(f"✅ Local enhancement complete! (via {agent_name})")
else:
from pathlib import Path
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
agent_name = agent or "claude"
print(f"✅ Local enhancement complete! (via {agent_name})")
except RuntimeError as e:
print(f"\n❌ Error: {e}", file=sys.stderr)

View File

@@ -1788,13 +1788,17 @@ def main() -> int:
print("❌ API enhancement not available. Falling back to LOCAL mode...")
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
else:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Quality Checker for Claude Skills
Quality Checker for AI Skills
Validates skill quality, checks links, and generates quality reports.
Usage:
@@ -350,7 +350,7 @@ class SkillQualityChecker:
else:
self.report.add_info(
"completeness",
"Consider adding prerequisites section - helps Claude verify conditions first",
"Consider adding prerequisites section - helps the LLM verify conditions first",
"SKILL.md",
)

View File

@@ -900,7 +900,7 @@ def main() -> int:
"0=disabled (default for RSS), 1=SKILL.md only, "
"2=+architecture/config, 3=full enhancement. "
"Mode selection: uses API if ANTHROPIC_API_KEY is set, "
"otherwise LOCAL (Claude Code)"
"otherwise LOCAL (Claude Code, Kimi, etc.)"
)
# RSS-specific arguments
@@ -1060,13 +1060,17 @@ def main() -> int:
print("❌ API enhancement not available. Falling back to LOCAL mode...")
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
else:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")

View File

@@ -268,7 +268,7 @@ class SignalFlowAnalyzer:
Args:
output_dir: Directory to save guides
ai_mode: "LOCAL" (Claude Code) or "API" (Anthropic API)
ai_mode: "LOCAL" (coding agent CLI) or "API" (Anthropic API)
Returns:
Path to generated guide file

View File

@@ -319,7 +319,7 @@ class UnifiedCodebaseAnalyzer:
c3x_data = {}
# C3.1: Design Patterns
patterns_file = output_dir / "patterns" / "design_patterns.json"
patterns_file = output_dir / "patterns" / "all_patterns.json"
if patterns_file.exists():
with open(patterns_file) as f:
patterns_data = json.load(f)

View File

@@ -19,12 +19,8 @@ Benefits:
import json
import logging
import os
import subprocess
import tempfile
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
logger = logging.getLogger(__name__)
@@ -89,58 +85,24 @@ class UnifiedEnhancer:
self.config.batch_size = cfg.get_local_batch_size()
self.config.parallel_workers = cfg.get_local_parallel_workers()
# Determine actual mode
self.api_key = self.config.api_key or os.environ.get("ANTHROPIC_API_KEY")
# Initialize AgentClient for AI calls
from skill_seekers.cli.agent_client import AgentClient
if self.config.mode == "auto":
if self.api_key:
self.config.mode = "api"
self._agent = AgentClient(
mode=self.config.mode,
api_key=self.config.api_key,
)
# Sync resolved mode back to config
self.config.mode = self._agent.mode
if self.config.enabled:
if self._agent.is_available():
self._agent.log_mode()
else:
self.config.mode = "local"
logger.info(" No API key found, using LOCAL mode (Claude Code CLI)")
# Initialize API client if needed
self.client = None
if self.config.mode == "api" and self.config.enabled:
try:
import anthropic
client_kwargs = {"api_key": self.api_key}
base_url = os.environ.get("ANTHROPIC_BASE_URL")
if base_url:
client_kwargs["base_url"] = base_url
logger.info(f"✅ Using custom API base URL: {base_url}")
self.client = anthropic.Anthropic(**client_kwargs)
logger.info("✅ AI enhancement enabled (using Claude API)")
except ImportError:
logger.warning("⚠️ anthropic package not installed, falling back to LOCAL mode")
self.config.mode = "local"
except Exception as e:
logger.warning(
f"⚠️ Failed to initialize API client: {e}, falling back to LOCAL mode"
)
self.config.mode = "local"
if self.config.mode == "local" and self.config.enabled:
if self._check_claude_cli():
logger.info("✅ AI enhancement enabled (using LOCAL mode - Claude Code CLI)")
else:
logger.warning("⚠️ Claude Code CLI not found. AI enhancement disabled.")
logger.warning("⚠️ AI agent not available. AI enhancement disabled.")
self.config.enabled = False
def _check_claude_cli(self) -> bool:
"""Check if Claude Code CLI is available."""
try:
result = subprocess.run(
["claude", "--version"],
capture_output=True,
text=True,
timeout=5,
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def enhance(
self,
items: list[dict],
@@ -265,88 +227,11 @@ class UnifiedEnhancer:
return items
def _call_claude(self, prompt: str, max_tokens: int = 1000) -> str | None:
"""Call Claude (API or LOCAL mode) with error handling."""
if self.config.mode == "api":
return self._call_claude_api(prompt, max_tokens)
elif self.config.mode == "local":
return self._call_claude_local(prompt)
return None
"""Call AI agent (API or LOCAL mode) via AgentClient."""
from skill_seekers.cli.agent_client import get_default_timeout
def _call_claude_api(self, prompt: str, max_tokens: int = 1000) -> str | None:
"""Call Claude API."""
if not self.client:
return None
try:
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
except Exception as e:
logger.warning(f"⚠️ API call failed: {e}")
return None
def _call_claude_local(self, prompt: str) -> str | None:
"""Call Claude Code CLI in LOCAL mode."""
try:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Write prompt to file
prompt_file = temp_path / "prompt.txt"
prompt_file.write_text(prompt)
# Output file
output_file = temp_path / "response.json"
# Call Claude CLI
result = subprocess.run(
[
"claude",
str(prompt_file),
"--output",
str(output_file),
"--model",
"sonnet",
],
capture_output=True,
text=True,
timeout=120,
cwd=str(temp_path),
)
if result.returncode != 0:
logger.warning(f"⚠️ Claude CLI returned error: {result.returncode}")
return None
# Read output
if output_file.exists():
response_text = output_file.read_text()
try:
json.loads(response_text)
return response_text
except json.JSONDecodeError:
# Try to extract JSON
import re
json_match = re.search(r"\[[\s\S]*\]|\{[\s\S]*\}", response_text)
if json_match:
return json_match.group()
return None
else:
for json_file in temp_path.glob("*.json"):
if json_file.name != "prompt.json":
return json_file.read_text()
return None
except subprocess.TimeoutExpired:
logger.warning("⚠️ Claude CLI timeout (2 minutes)")
return None
except Exception as e:
logger.warning(f"⚠️ LOCAL mode error: {e}")
return None
timeout = get_default_timeout() if self._agent.mode == "local" else None
return self._agent.call(prompt, max_tokens=max_tokens, timeout=timeout)
def _get_default_prompt(self, enhancement_type: str) -> str:
"""Get default prompt for enhancement type."""

View File

@@ -9,7 +9,7 @@ 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
skill-seekers unified --config configs/react_unified.json --merge-mode ai-enhanced
"""
import argparse
@@ -24,9 +24,10 @@ from typing import Any
# Import validators and scrapers
try:
from skill_seekers.cli.agent_client import get_default_timeout
from skill_seekers.cli.config_validator import validate_config
from skill_seekers.cli.conflict_detector import ConflictDetector
from skill_seekers.cli.merge_sources import ClaudeEnhancedMerger, RuleBasedMerger
from skill_seekers.cli.merge_sources import AIEnhancedMerger, RuleBasedMerger
from skill_seekers.cli.unified_skill_builder import UnifiedSkillBuilder
from skill_seekers.cli.utils import setup_logging
except ImportError as e:
@@ -45,7 +46,7 @@ class UnifiedScraper:
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)
4. Merge intelligently (rule-based or AI-enhanced)
5. Build unified skill
"""
@@ -64,8 +65,9 @@ class UnifiedScraper:
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")
# Determine merge mode (normalize claude-enhanced → ai-enhanced for backward compat)
raw_mode = merge_mode or self.config.get("merge_mode", "rule-based")
self.merge_mode = "ai-enhanced" if raw_mode == "claude-enhanced" else raw_mode
logger.info(f"Merge mode: {self.merge_mode}")
# Storage for scraped data - use lists to support multiple sources of same type
@@ -225,7 +227,7 @@ class UnifiedScraper:
"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),
"max_pages": source.get("max_pages", 500),
}
# Pass through llms.txt settings (so unified configs behave the same as doc_scraper configs)
@@ -239,9 +241,21 @@ class UnifiedScraper:
if "start_urls" in source:
doc_source["start_urls"] = source["start_urls"]
# Pass through browser rendering settings
if source.get("browser"):
doc_source["browser"] = True
if "browser_wait_until" in source:
doc_source["browser_wait_until"] = source["browser_wait_until"]
if "browser_extra_wait" in source:
doc_source["browser_extra_wait"] = source["browser_extra_wait"]
doc_config = {
"name": f"{self.name}_docs",
"description": f"Documentation for {self.name}",
"base_url": source["base_url"],
"browser": source.get("browser", False),
"browser_wait_until": source.get("browser_wait_until", "domcontentloaded"),
"browser_extra_wait": source.get("browser_extra_wait", 0),
"sources": [doc_source],
}
@@ -256,7 +270,29 @@ class UnifiedScraper:
doc_scraper_path = Path(__file__).parent / "doc_scraper.py"
cmd = [sys.executable, str(doc_scraper_path), "--config", temp_config_path, "--fresh"]
result = subprocess.run(cmd, capture_output=True, text=True, stdin=subprocess.DEVNULL)
# Forward agent-related CLI args so doc scraper enhancement respects
# the user's chosen agent instead of defaulting to claude.
cli_args = getattr(self, "_cli_args", None)
if cli_args is not None:
if getattr(cli_args, "agent", None):
cmd.extend(["--agent", cli_args.agent])
if getattr(cli_args, "agent_cmd", None):
cmd.extend(["--agent-cmd", cli_args.agent_cmd])
if getattr(cli_args, "api_key", None):
cmd.extend(["--api-key", cli_args.api_key])
# Support "browser": true in source config for JavaScript SPA sites
if source.get("browser", False):
cmd.append("--browser")
logger.info(" 🌐 Browser mode enabled (JavaScript rendering via Playwright)")
try:
result = subprocess.run(
cmd, capture_output=True, text=True, stdin=subprocess.DEVNULL, timeout=3600
)
except subprocess.TimeoutExpired:
logger.error("Documentation scraping timed out after 60 minutes")
return
if result.returncode != 0:
logger.error(f"Documentation scraping failed with return code {result.returncode}")
@@ -318,6 +354,11 @@ class UnifiedScraper:
shutil.move(docs_data_dir, cache_data_dir)
logger.info(f"📦 Moved docs data to cache: {cache_data_dir}")
# Update data_file path to point to cache location
if self.scraped_data["documentation"]:
cached_data_file = os.path.join(cache_data_dir, "summary.json")
self.scraped_data["documentation"][-1]["data_file"] = cached_data_file
def _clone_github_repo(self, repo_name: str, idx: int = 0) -> str | None:
"""
Clone GitHub repository to cache directory for C3.x analysis.
@@ -353,7 +394,7 @@ class UnifiedScraper:
["git", "clone", repo_url, clone_path],
capture_output=True,
text=True,
timeout=600, # 10 minute timeout for full clone
timeout=get_default_timeout(), # default 45 min, configurable via SKILL_SEEKER_ENHANCE_TIMEOUT
)
if result.returncode == 0:
@@ -422,6 +463,7 @@ class UnifiedScraper:
"include_code": source.get("include_code", True),
"code_analysis_depth": source.get("code_analysis_depth", "surface"),
"file_patterns": source.get("file_patterns", []),
"language": source.get("language", ""),
"local_repo_path": local_repo_path, # Use cloned path if available
}
@@ -776,25 +818,38 @@ class UnifiedScraper:
enhance_level=enhance_level,
)
# Load analysis outputs into memory
# Load analysis outputs into memory.
# _generate_references() moves dirs into references/ and deletes originals.
refs = temp_output / "references"
local_data = {
"source_id": f"{self.name}_local_{idx}_{path_id}",
"path": local_path,
"name": source_name,
"description": source.get("description", f"Local analysis of {path_id}"),
"weight": source.get("weight", 1.0),
"patterns": self._load_json(temp_output / "patterns" / "detected_patterns.json"),
"test_examples": self._load_json(
temp_output / "test_examples" / "test_examples.json"
"patterns": self._load_json_fallback(
refs / "patterns" / "all_patterns.json",
temp_output / "patterns" / "all_patterns.json",
),
"how_to_guides": self._load_guide_collection(temp_output / "tutorials"),
"config_patterns": self._load_json(
temp_output / "config_patterns" / "config_patterns.json"
"test_examples": self._load_json_fallback(
refs / "test_examples" / "test_examples.json",
temp_output / "test_examples" / "test_examples.json",
),
"architecture": self._load_json(temp_output / "ARCHITECTURE.json"),
"api_reference": self._load_api_reference(temp_output / "api_reference"),
"dependency_graph": self._load_json(
temp_output / "dependencies" / "dependency_graph.json"
"how_to_guides": self._load_guide_collection(refs / "tutorials")
or self._load_guide_collection(temp_output / "tutorials"),
"config_patterns": self._load_json_fallback(
refs / "config_patterns" / "config_patterns.json",
temp_output / "config_patterns" / "config_patterns.json",
),
"architecture": self._load_json_fallback(
refs / "architecture" / "architectural_patterns.json",
temp_output / "architecture" / "architectural_patterns.json",
),
"api_reference": self._load_api_reference(refs / "api_reference")
or self._load_api_reference(temp_output / "api_reference"),
"dependency_graph": self._load_json_fallback(
refs / "dependencies" / "dependency_graph.json",
temp_output / "dependencies" / "dependency_graph.json",
),
}
@@ -1425,6 +1480,12 @@ class UnifiedScraper:
logger.info(f"✅ Chat: {len(chat_data.get('messages', []))} messages extracted")
def _load_json_fallback(self, primary: Path, fallback: Path) -> dict:
"""Load JSON from primary path, falling back to secondary if not found."""
if primary.exists():
return self._load_json(primary)
return self._load_json(fallback)
def _load_json(self, file_path: Path) -> dict:
"""
Load JSON file safely.
@@ -1525,6 +1586,11 @@ class UnifiedScraper:
logger.info(f" Analyzing codebase: {local_repo_path}")
try:
# Resolve agent from CLI args for C3.x analysis
cli_args = getattr(self, "_cli_args", None)
agent = getattr(cli_args, "agent", None) if cli_args else None
agent_cmd = getattr(cli_args, "agent_cmd", None) if cli_args else None
# Run full C3.x analysis
_results = analyze_codebase(
directory=Path(local_repo_path),
@@ -1541,25 +1607,39 @@ class UnifiedScraper:
extract_config_patterns=True, # C3.4: Config patterns
extract_docs=True,
enhance_level=0 if source.get("ai_mode", "auto") == "none" else 2,
agent=agent,
agent_cmd=agent_cmd,
)
# Load C3.x outputs into memory
# Load C3.x outputs into memory.
# _generate_references() inside analyze_codebase() moves analysis dirs
# into references/ and deletes the originals, so we check both locations.
refs = temp_output / "references"
c3_data = {
"patterns": self._load_json(temp_output / "patterns" / "detected_patterns.json"),
"test_examples": self._load_json(
temp_output / "test_examples" / "test_examples.json"
"patterns": self._load_json_fallback(
refs / "patterns" / "all_patterns.json",
temp_output / "patterns" / "all_patterns.json",
),
"how_to_guides": self._load_guide_collection(temp_output / "tutorials"),
"config_patterns": self._load_json(
temp_output / "config_patterns" / "config_patterns.json"
"test_examples": self._load_json_fallback(
refs / "test_examples" / "test_examples.json",
temp_output / "test_examples" / "test_examples.json",
),
"architecture": self._load_json(
temp_output / "architecture" / "architectural_patterns.json"
"how_to_guides": self._load_guide_collection(refs / "tutorials")
or self._load_guide_collection(temp_output / "tutorials"),
"config_patterns": self._load_json_fallback(
refs / "config_patterns" / "config_patterns.json",
temp_output / "config_patterns" / "config_patterns.json",
),
"architecture": self._load_json_fallback(
refs / "architecture" / "architectural_patterns.json",
temp_output / "architecture" / "architectural_patterns.json",
),
"api_reference": self._load_api_reference(refs / "api_reference")
or self._load_api_reference(temp_output / "api_reference"),
"dependency_graph": self._load_json_fallback(
refs / "dependencies" / "dependency_graph.json",
temp_output / "dependencies" / "dependency_graph.json",
),
"api_reference": self._load_api_reference(temp_output / "api_reference"), # C2.5
"dependency_graph": self._load_json(
temp_output / "dependencies" / "dependency_graph.json"
), # C2.6
}
# Log summary
@@ -1607,16 +1687,23 @@ class UnifiedScraper:
if not self.validator.needs_api_merge():
logger.info("No API merge needed (only one API source)")
logger.info("\n" + "=" * 60)
logger.info("PHASE 3: Merging sources (skipped - no conflicts detected)")
logger.info("=" * 60)
return []
# Get documentation and GitHub data
docs_data = self.scraped_data.get("documentation", {})
github_data = self.scraped_data.get("github", {})
# Get documentation and GitHub data (scraped_data stores lists of sources)
docs_list = self.scraped_data.get("documentation", [])
github_list = self.scraped_data.get("github", [])
if not docs_data or not github_data:
if not docs_list or not github_list:
logger.warning("Missing documentation or GitHub data for conflict detection")
return []
# Use the first source from each list
docs_data = docs_list[0]
github_data = github_list[0]
# Load data files
with open(docs_data["data_file"], encoding="utf-8") as f:
docs_json = json.load(f)
@@ -1663,9 +1750,16 @@ class UnifiedScraper:
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", {})
# Get data files (scraped_data stores lists of sources)
docs_list = self.scraped_data.get("documentation", [])
github_list = self.scraped_data.get("github", [])
if not docs_list or not github_list:
logger.warning("Missing documentation or GitHub data for merging")
return None
docs_data = docs_list[0]
github_data = github_list[0]
# Load data
with open(docs_data["data_file"], encoding="utf-8") as f:
@@ -1675,8 +1769,8 @@ class UnifiedScraper:
github_json = json.load(f)
# Choose merger
if self.merge_mode == "claude-enhanced":
merger = ClaudeEnhancedMerger(docs_json, github_json, conflicts)
if self.merge_mode in ("ai-enhanced", "claude-enhanced"):
merger = AIEnhancedMerger(docs_json, github_json, conflicts)
else:
merger = RuleBasedMerger(docs_json, github_json, conflicts)
@@ -1797,6 +1891,124 @@ class UnifiedScraper:
}
run_workflows(effective_args, context=unified_context)
# Phase 6: AI Enhancement of SKILL.md
# Triggered by config "enhancement" block or CLI --enhance-level
enhancement_config = self.config.get("enhancement", {})
enhancement_enabled = enhancement_config.get("enabled", False)
enhancement_level = enhancement_config.get("level", 0)
enhancement_mode = enhancement_config.get("mode", "AUTO").upper()
# CLI --enhance-level overrides config
cli_enhance_level = getattr(args, "enhance_level", None) if args is not None else None
if cli_enhance_level is not None:
enhancement_enabled = cli_enhance_level > 0
enhancement_level = cli_enhance_level
if enhancement_enabled and enhancement_level > 0:
logger.info("\n" + "=" * 60)
logger.info(
f"PHASE 6: Enhancing SKILL.md (level {enhancement_level}, mode {enhancement_mode})"
)
logger.info("=" * 60)
skill_md_path = os.path.join(self.output_dir, "SKILL.md")
if not os.path.exists(skill_md_path):
logger.warning("⚠️ SKILL.md not found, skipping enhancement")
elif enhancement_mode == "LOCAL":
try:
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
# Get agent from CLI args, config enhancement block, or env var
agent = None
agent_cmd = None
if args is not None:
agent = getattr(args, "agent", None)
agent_cmd = getattr(args, "agent_cmd", None)
if not agent:
agent = enhancement_config.get("agent", None)
if not agent:
agent = os.environ.get("SKILL_SEEKER_AGENT", "").strip() or None
# Read timeout from config enhancement block
timeout_val = enhancement_config.get("timeout")
if timeout_val is not None:
if isinstance(timeout_val, str) and timeout_val.lower() in (
"unlimited",
"none",
):
timeout_val = 86400 # 24 hours
else:
try:
timeout_val = int(timeout_val)
if timeout_val <= 0:
timeout_val = 86400
except (ValueError, TypeError):
timeout_val = 2700
else:
timeout_val = 2700
enhancer = LocalSkillEnhancer(
self.output_dir, force=True, agent=agent, agent_cmd=agent_cmd
)
success = enhancer.run(headless=True, timeout=timeout_val)
agent_name = agent or "claude"
if success:
logger.info(f"✅ SKILL.md enhanced (LOCAL mode via {agent_name})")
else:
logger.warning(
f"⚠️ SKILL.md enhancement returned false (LOCAL mode via {agent_name}). "
"Check logs above for the exact error."
)
except Exception as e:
logger.warning(f"⚠️ LOCAL enhancement failed: {e}")
logger.info(
" Try manually: skill-seekers enhance "
+ self.output_dir
+ " --agent kimi"
)
else:
# API mode — use AgentClient for multi-provider support
try:
from skill_seekers.cli.agent_client import AgentClient
client = AgentClient(mode="api")
if client.client:
# Read references and current SKILL.md
references = ""
refs_dir = Path(self.output_dir) / "references"
if refs_dir.exists():
for md_file in sorted(refs_dir.rglob("*.md")):
content = md_file.read_text(encoding="utf-8", errors="ignore")
references += f"\n\n## {md_file.name}\n\n{content}"
current_skill = Path(skill_md_path).read_text(encoding="utf-8")
# Build enhancement prompt
prompt = (
f"Enhance this SKILL.md using the reference documentation.\n\n"
f"CURRENT SKILL.MD:\n{current_skill}\n\n"
f"REFERENCES:\n{references}\n\n"
f"Return ONLY the complete enhanced SKILL.md content, "
f"starting with the frontmatter (---)."
)
enhanced = client.call(prompt, max_tokens=8192)
if enhanced:
shutil.copy2(skill_md_path, skill_md_path + ".backup")
Path(skill_md_path).write_text(enhanced, encoding="utf-8")
logger.info(
f"✅ SKILL.md enhanced (API mode via {client.provider})"
)
else:
logger.warning("⚠️ API enhancement returned empty result")
else:
logger.warning("⚠️ No API key found, skipping API enhancement")
logger.info(' Set an API key or use "mode": "LOCAL" in config')
except Exception as e:
logger.warning(f"⚠️ API enhancement failed: {e}")
else:
logger.info("\n" + "=" * 60)
logger.info("PHASE 6: Enhancement (skipped - not enabled in config)")
logger.info("=" * 60)
logger.info("\n" + "" * 20)
logger.info("Unified scraping complete!")
logger.info("" * 20 + "\n")
@@ -1826,7 +2038,7 @@ Examples:
skill-seekers unified --config configs/godot_unified.json
# Override merge mode
skill-seekers unified --config configs/react_unified.json --merge-mode claude-enhanced
skill-seekers unified --config configs/react_unified.json --merge-mode ai-enhanced
# Backward compatible with legacy configs
skill-seekers unified --config configs/react.json
@@ -1837,8 +2049,8 @@ Examples:
parser.add_argument(
"--merge-mode",
"-m",
choices=["rule-based", "claude-enhanced"],
help="Override config merge mode",
choices=["rule-based", "ai-enhanced", "claude-enhanced"],
help="Override config merge mode (ai-enhanced or rule-based). 'claude-enhanced' accepted as alias.",
)
parser.add_argument(
"--skip-codebase-analysis",
@@ -1901,6 +2113,19 @@ Examples:
"Overrides per-source enhance_level in config."
),
)
parser.add_argument(
"--agent",
type=str,
choices=["claude", "codex", "copilot", "opencode", "kimi", "custom"],
metavar="AGENT",
help="Local coding agent for enhancement (default: AI agent from SKILL_SEEKER_AGENT env var)",
)
parser.add_argument(
"--agent-cmd",
type=str,
metavar="CMD",
help="Override agent command template (advanced)",
)
args = parser.parse_args()
setup_logging()

View File

@@ -4,7 +4,7 @@ Automatic Skill Uploader
Uploads a skill package to LLM platforms (Claude, Gemini, OpenAI, etc.)
Usage:
# Claude (default)
# Anthropic (default)
export ANTHROPIC_API_KEY=sk-ant-...
skill-seekers upload output/react.zip
@@ -117,7 +117,7 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Setup:
Claude:
Anthropic (Claude):
export ANTHROPIC_API_KEY=sk-ant-...
Gemini:
@@ -135,7 +135,7 @@ Setup:
docker run -p 8080:8080 semitechnologies/weaviate:latest
Examples:
# Upload to Claude (default)
# Upload to default platform
skill-seekers upload output/react.zip
# Upload to Gemini
@@ -162,9 +162,9 @@ Examples:
parser.add_argument(
"--target",
choices=["claude", "gemini", "openai", "chroma", "weaviate"],
default="claude",
help="Target platform (default: claude)",
choices=["claude", "gemini", "openai", "kimi", "chroma", "weaviate"],
default=None,
help="Target platform (auto-detected from API keys, or 'claude' if none set)",
)
parser.add_argument("--api-key", help="Platform API key (or set environment variable)")
@@ -209,6 +209,12 @@ Examples:
args = parser.parse_args()
# Auto-detect target platform if not specified
if args.target is None:
from skill_seekers.cli.agent_client import AgentClient
args.target = AgentClient.detect_default_target()
# Build kwargs for vector DB upload
upload_kwargs = {}

View File

@@ -76,32 +76,48 @@ def open_folder(folder_path: str | Path) -> bool:
def has_api_key() -> bool:
"""
Check if ANTHROPIC_API_KEY is set in environment
Check if any AI API key is set in environment.
Checks: ANTHROPIC_API_KEY, MOONSHOT_API_KEY, GOOGLE_API_KEY, OPENAI_API_KEY
Returns:
bool: True if API key is set, False otherwise
bool: True if any API key is set, False otherwise
"""
api_key = os.environ.get("ANTHROPIC_API_KEY", "").strip()
return len(api_key) > 0
for env_var in ("ANTHROPIC_API_KEY", "MOONSHOT_API_KEY", "GOOGLE_API_KEY", "OPENAI_API_KEY"):
if os.environ.get(env_var, "").strip():
return True
return False
def get_api_key() -> str | None:
"""
Get ANTHROPIC_API_KEY from environment
Get the first available AI API key from environment.
Checks: ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, MOONSHOT_API_KEY,
GOOGLE_API_KEY, OPENAI_API_KEY
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
for env_var in (
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"MOONSHOT_API_KEY",
"GOOGLE_API_KEY",
"OPENAI_API_KEY",
):
key = os.environ.get(env_var, "").strip()
if key:
return key
return None
def get_upload_url() -> str:
"""
Get the Claude skills upload URL
Get the skills upload URL
Returns:
str: Claude skills upload URL
str: Skills upload URL
"""
return "https://claude.ai/skills"
@@ -120,7 +136,7 @@ def print_upload_instructions(zip_path: str | Path) -> None:
print("║ NEXT STEP ║")
print("╚══════════════════════════════════════════════════════════╝")
print()
print(f"📤 Upload to Claude: {get_upload_url()}")
print(f"📤 Upload to platform: {get_upload_url()}")
print()
print(f"1. Go to {get_upload_url()}")
print('2. Click "Upload Skill"')

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""
Video to Claude Skill Converter
Video to AI Skill Converter
Extracts transcripts, metadata, and visual content from videos
and converts them into Claude AI skills.
and converts them into AI skills.
Supports YouTube videos/playlists, Vimeo, and local video files.
@@ -264,7 +264,7 @@ def _is_likely_code(text: str) -> bool:
def _ai_clean_reference(ref_path: str, content: str, api_key: str | None = None) -> None:
"""Use AI to clean Code Timeline section in a reference file.
Sends the reference file content to Claude with a focused prompt
Sends the reference file content to the AI with a focused prompt
to reconstruct the Code Timeline from noisy OCR + transcript context.
"""
try:
@@ -300,7 +300,7 @@ def _ai_clean_reference(ref_path: str, content: str, api_key: str | None = None)
try:
client = anthropic.Anthropic(**client_kwargs)
response = client.messages.create(
model="claude-sonnet-4-20250514",
model=os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514"),
max_tokens=8000,
messages=[{"role": "user", "content": prompt}],
)
@@ -319,7 +319,7 @@ def _ai_clean_reference(ref_path: str, content: str, api_key: str | None = None)
class VideoToSkillConverter:
"""Convert video content to Claude skill."""
"""Convert video content to AI skill."""
def __init__(self, config: dict):
"""Initialize converter.
@@ -865,9 +865,12 @@ class VideoToSkillConverter:
"""First-pass: AI-clean reference files before SKILL.md enhancement.
When enhance_level >= 2 and an API key is available, sends each
reference file to Claude to reconstruct noisy Code Timeline
reference file to the AI to reconstruct noisy Code Timeline
sections using transcript context.
"""
# Note: Middle-layer AI cleaning currently only supports Anthropic API
# For other agents (kimi, etc.), this step is skipped and enhancement
# happens at the SKILL.md level instead of per-reference-file
has_api_key = bool(
os.environ.get("ANTHROPIC_API_KEY")
or os.environ.get("ANTHROPIC_AUTH_TOKEN")
@@ -1203,9 +1206,12 @@ def _run_video_enhancement(skill_dir: str, enhance_level: int, args) -> None:
os.environ.get("ANTHROPIC_API_KEY")
or os.environ.get("ANTHROPIC_AUTH_TOKEN")
or getattr(args, "api_key", None)
or os.environ.get("MOONSHOT_API_KEY")
)
if not has_api_key:
agent = getattr(args, "agent", None)
if not has_api_key and not agent:
logger.info("\n💡 Enhance your video skill with AI:")
logger.info(f" export ANTHROPIC_API_KEY=sk-ant-...")
logger.info(f" skill-seekers enhance {skill_dir} --enhance-level {enhance_level}")
@@ -1215,10 +1221,11 @@ def _run_video_enhancement(skill_dir: str, enhance_level: int, args) -> None:
try:
enhance_cmd = ["skill-seekers-enhance", skill_dir]
enhance_cmd.extend(["--enhance-level", str(enhance_level)])
api_key = getattr(args, "api_key", None)
if api_key:
enhance_cmd.extend(["--api-key", api_key])
if agent:
enhance_cmd.extend(["--agent", agent])
logger.info(
"Starting video skill enhancement (this may take 10+ minutes "

View File

@@ -590,7 +590,7 @@ def _ocr_with_claude_vision(frame_path: str, frame_type: FrameType) -> tuple[str
client = anthropic.Anthropic(api_key=api_key)
response = client.messages.create(
model="claude-haiku-4-5-20251001",
model=os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5-20251001"),
max_tokens=4096,
messages=[
{

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""
Word Document (.docx) to Claude Skill Converter (Task B2)
Word Document (.docx) to AI Skill Converter (Task B2)
Converts Word documents into Claude AI skills.
Converts Word documents into AI skills.
Uses mammoth for HTML conversion and python-docx for metadata/tables.
Usage:
@@ -73,7 +73,7 @@ def infer_description_from_word(metadata: dict = None, name: str = "") -> str:
class WordToSkillConverter:
"""Convert Word document (.docx) to Claude skill."""
"""Convert Word document (.docx) to AI skill."""
def __init__(self, config):
self.config = config
@@ -924,7 +924,7 @@ def main():
from .arguments.word import add_word_arguments
parser = argparse.ArgumentParser(
description="Convert Word document (.docx) to Claude skill",
description="Convert Word document (.docx) to AI skill",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -1031,14 +1031,18 @@ def main():
from pathlib import Path
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")
else:
from pathlib import Path
from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
enhancer = LocalSkillEnhancer(Path(skill_dir))
agent = getattr(args, "agent", None) if args else None
agent_cmd = getattr(args, "agent_cmd", None) if args else None
enhancer = LocalSkillEnhancer(Path(skill_dir), agent=agent, agent_cmd=agent_cmd)
enhancer.run(headless=True)
print("✅ Local enhancement complete!")

View File

@@ -43,6 +43,7 @@ def _build_inline_engine(args: argparse.Namespace):
"""Build a WorkflowEngine from --enhance-stage flags."""
from skill_seekers.cli.enhancement_workflow import WorkflowEngine
agent = getattr(args, "agent", None)
stages = []
for i, spec in enumerate(args.enhance_stage, 1):
if ":" in spec:
@@ -63,7 +64,7 @@ def _build_inline_engine(args: argparse.Namespace):
"description": "Custom inline workflow from --enhance-stage arguments",
"stages": stages,
}
return WorkflowEngine(workflow_data=inline_def)
return WorkflowEngine(workflow_data=inline_def, agent=agent)
def run_workflows(
@@ -107,6 +108,7 @@ def run_workflows(
logger.info(f" {k} = {v}")
executed: list[str] = []
agent = getattr(args, "agent", None)
# ── Named workflows ────────────────────────────────────────────────────
total = len(named_workflows) + (1 if inline_stages else 0)
@@ -118,7 +120,7 @@ def run_workflows(
logger.info(header)
try:
engine = WorkflowEngine(workflow_name)
engine = WorkflowEngine(workflow_name, agent=agent)
except Exception as exc:
logger.error(f"❌ Failed to load workflow '{workflow_name}': {exc}")
logger.info(" Skipping this workflow and continuing...")

View File

@@ -1,10 +1,13 @@
# Skill Seeker MCP Server
Model Context Protocol (MCP) server for Skill Seeker - enables Claude Code to generate documentation skills directly.
> Works with **Claude Code**, **Cursor**, **Windsurf**, **VS Code + Cline**, and **IntelliJ IDEA**.
> Supports API mode (Anthropic, Moonshot/Kimi, Google Gemini, OpenAI) and LOCAL mode (any AI coding agent).
Model Context Protocol (MCP) server for Skill Seeker - enables AI coding agents 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:
This MCP server allows your AI coding agent to use Skill Seeker's tools directly through natural language commands. Instead of running CLI commands manually, you can ask your agent to:
- Generate config files for any documentation site
- Estimate page counts before scraping
@@ -36,12 +39,12 @@ pip3 install -e ".[mcp]"
# - Install dependencies
# - Test the server
# - Generate configuration
# - Guide you through Claude Code setup
# - Guide you through agent setup
```
### 3. Manual Setup
Add to `~/.claude.json`:
**For Claude Code** - add to `~/.claude.json`:
```json
{
@@ -62,13 +65,15 @@ Add to `~/.claude.json`:
**Replace `/path/to/Skill_Seekers`** with your actual repository path!
### 4. Restart Claude Code
**For Cursor/Windsurf** - use HTTP transport mode. See your editor's MCP documentation for configuration details.
Quit and reopen Claude Code (don't just close the window).
### 4. Restart Your Agent
Quit and reopen your AI coding agent (don't just close the window).
### 5. Test
In Claude Code, type:
In your AI coding agent, type:
```
List all available configs
```
@@ -107,7 +112,7 @@ Estimate pages for configs/react.json
```
### 3. `scrape_docs`
Scrape documentation and build Claude skill.
Scrape documentation and build LLM skill.
**Parameters:**
- `config_path` (required): Path to config file
@@ -125,16 +130,17 @@ Package skill directory into platform-specific format. Automatically uploads if
**Parameters:**
- `skill_dir` (required): Path to skill directory (e.g., "output/react/")
- `target` (optional): Target platform - "claude", "gemini", "openai", "markdown" (default: "claude")
- `target` (optional): Target platform - "claude", "gemini", "openai", "markdown", and more (default: auto-detected from environment)
- `auto_upload` (optional): Try to upload automatically if API key is available (default: true)
**Platform-specific outputs:**
- Claude/OpenAI/Markdown: `.zip` file
- Claude/OpenAI/Markdown/Kimi/DeepSeek/Qwen: `.zip` file
- Gemini: `.tar.gz` file
**Examples:**
```
Package skill for Claude (default): output/react/
Package skill (auto-detected platform): output/react/
Package skill for Claude: output/react/ with target claude
Package skill for Gemini: output/react/ with target gemini
Package skill for OpenAI: output/react/ with target openai
Package skill for Markdown: output/react/ with target markdown
@@ -145,7 +151,7 @@ Upload skill package to target LLM platform (requires platform-specific API key)
**Parameters:**
- `skill_zip` (required): Path to skill package (`.zip` or `.tar.gz`)
- `target` (optional): Target platform - "claude", "gemini", "openai" (default: "claude")
- `target` (optional): Target platform - "claude", "gemini", "openai" (default: auto-detected from environment)
**Examples:**
```
@@ -161,8 +167,8 @@ Enhance SKILL.md with AI using target platform's model. Transforms basic templat
**Parameters:**
- `skill_dir` (required): Path to skill directory (e.g., "output/react/")
- `target` (optional): Target platform - "claude", "gemini", "openai" (default: "claude")
- `mode` (optional): "local" (Claude Code Max, no API key) or "api" (requires API key) (default: "local")
- `target` (optional): Target platform - "claude", "gemini", "openai" (default: auto-detected from environment)
- `mode` (optional): "local" (AI coding agent, no API key) or "api" (requires API key) (default: "local")
- `api_key` (optional): Platform API key (uses env var if not provided)
**What it does:**
@@ -173,12 +179,12 @@ Enhance SKILL.md with AI using target platform's model. Transforms basic templat
**Examples:**
```
Enhance with Claude locally (no API key): output/react/
Enhance locally (no API key): output/react/
Enhance with Gemini API: output/react/ with target gemini and mode api
Enhance with OpenAI API: output/react/ with target openai and mode api
```
**Note:** Local mode uses Claude Code Max (requires Claude Code but no API key). API mode requires platform-specific API key.
**Note:** Local mode uses your AI coding agent (no API key needed). API mode requires a platform-specific API key.
### 7. `list_configs`
List all available preset configurations.
@@ -240,7 +246,7 @@ Generate router for configs/godot-*.json
- Users can ask questions naturally, router directs to appropriate sub-skill
### 11. `scrape_pdf`
Scrape PDF documentation and build Claude skill. Extracts text, code blocks, images, and tables from PDF files with advanced features.
Scrape PDF documentation and build LLM 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")
@@ -293,20 +299,20 @@ Fast parallel processing: --pdf docs/large.pdf --parallel --workers 8
```
User: Generate config for Svelte at https://svelte.dev/docs
Claude: ✅ Config created: configs/svelte.json
Agent: ✅ Config created: configs/svelte.json
User: Estimate pages for configs/svelte.json
Claude: 📊 Estimated pages: 150
Agent: 📊 Estimated pages: 150
User: Scrape docs using configs/svelte.json
Claude: ✅ Skill created at output/svelte/
Agent: ✅ Skill created at output/svelte/
User: Package skill at output/svelte/
Claude: ✅ Created: output/svelte.zip
Ready to upload to Claude!
Agent: ✅ Created: output/svelte.zip
Ready to upload!
```
### Use Existing Preset
@@ -314,15 +320,15 @@ Claude: ✅ Created: output/svelte.zip
```
User: List all available configs
Claude: [Shows all configs: godot, react, vue, django, fastapi, etc.]
Agent: [Shows all configs: godot, react, vue, django, fastapi, etc.]
User: Scrape docs using configs/react.json
Claude: ✅ Skill created at output/react/
Agent: ✅ Skill created at output/react/
User: Package skill at output/react/
Claude: ✅ Created: output/react.zip
Agent: ✅ Created: output/react.zip
```
### Validate Before Scraping
@@ -330,7 +336,7 @@ Claude: ✅ Created: output/react.zip
```
User: Validate configs/godot.json
Claude: ✅ Config is valid!
Agent: ✅ Config is valid!
Name: godot
Base URL: https://docs.godotengine.org/en/stable/
Max pages: 500
@@ -338,7 +344,7 @@ Claude: ✅ Config is valid!
User: Scrape docs using configs/godot.json
Claude: [Starts scraping...]
Agent: [Starts scraping...]
```
### PDF Documentation - NEW
@@ -346,17 +352,17 @@ Claude: [Starts scraping...]
```
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/
Agent: 📄 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!
Agent: ✅ Created: output/api-docs.zip
Ready to upload!
```
### Large Documentation (40K Pages)
@@ -364,13 +370,13 @@ Claude: ✅ Created: output/api-docs.zip
```
User: Estimate pages for configs/godot.json
Claude: 📊 Estimated pages: 40,000
⚠️ Large documentation detected!
💡 Recommend splitting into multiple skills
Agent: 📊 Estimated pages: 40,000
⚠️ Large documentation detected!
💡 Recommend splitting into multiple skills
User: Split configs/godot.json using router strategy
Claude: ✅ Split complete!
Agent: ✅ Split complete!
Created 5 sub-skills:
- godot-scripting.json (5,000 pages)
- godot-2d.json (8,000 pages)
@@ -380,12 +386,12 @@ Claude: ✅ Split complete!
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!
Agent: [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/
Agent: ✅ Router skill created at output/godot/
Routing logic:
- "scripting", "gdscript" → godot-scripting
- "2d", "sprites", "tilemap" → godot-2d
@@ -395,16 +401,16 @@ Claude: ✅ Router skill created at output/godot/
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
Agent: ✅ 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!
Upload all to your LLM platform!
Users just ask questions naturally - router handles routing!
```
## Architecture
@@ -420,11 +426,11 @@ mcp/
### How It Works
1. **Claude Code** sends MCP requests to the server
1. **AI coding agent** (Claude Code, Cursor, Windsurf, etc.) 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
5. **Results** returned to the agent via MCP protocol
### Tool Implementation
@@ -482,12 +488,12 @@ python3 -m pytest tests/test_mcp_server.py -v
### MCP Server Not Loading
**Symptoms:**
- Tools don't appear in Claude Code
- Tools don't appear in your AI coding agent
- No response to skill-seeker commands
**Solutions:**
1. Check configuration:
1. Check configuration (for Claude Code):
```bash
cat ~/.config/claude-code/mcp.json
```
@@ -503,11 +509,12 @@ python3 -m pytest tests/test_mcp_server.py -v
pip3 install -r mcp/requirements.txt
```
4. Completely restart Claude Code (quit and reopen)
4. Completely restart your AI coding agent (quit and reopen)
5. Check Claude Code logs:
- macOS: `~/Library/Logs/Claude Code/`
- Linux: `~/.config/claude-code/logs/`
5. Check agent logs:
- Claude Code (macOS): `~/Library/Logs/Claude Code/`
- Claude Code (Linux): `~/.config/claude-code/logs/`
- Cursor/Windsurf: Check your editor's output panel for MCP errors
### "ModuleNotFoundError: No module named 'mcp'"
@@ -551,7 +558,7 @@ pip install requests beautifulsoup4
which python3 # Copy this path
```
Configure Claude Code to use venv Python:
Configure your AI coding agent to use venv Python (example for Claude Code):
```json
{

View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""
Config Publisher
Pushes validated config files to registered config source repositories.
Follows the same pattern as MarketplacePublisher but for configs.
"""
import json
import logging
import os
import shutil
from pathlib import Path
logger = logging.getLogger(__name__)
# Category keywords for auto-detection
CATEGORY_KEYWORDS = {
"game-engines": ["unity", "godot", "unreal", "gamemaker", "spine", "dotween", "addressable"],
"web-frameworks": [
"react",
"vue",
"angular",
"next",
"nuxt",
"svelte",
"django",
"flask",
"fastapi",
"express",
],
"ai-ml": ["tensorflow", "pytorch", "langchain", "llama", "openai", "anthropic", "huggingface"],
"databases": ["postgres", "mysql", "mongo", "redis", "sqlite", "prisma", "drizzle"],
"devops": ["docker", "kubernetes", "terraform", "ansible", "jenkins", "github-actions"],
"cloud": ["aws", "gcp", "azure", "vercel", "netlify", "cloudflare"],
"mobile": ["flutter", "react-native", "swift", "kotlin", "expo"],
"testing": ["jest", "pytest", "cypress", "playwright", "vitest"],
"build-tools": ["webpack", "vite", "esbuild", "turbo", "nx"],
"css-frameworks": ["tailwind", "bootstrap", "material-ui", "chakra"],
"security": ["oauth", "jwt", "auth0", "keycloak"],
"development-tools": ["git", "vscode", "neovim", "cursor"],
"messaging": ["kafka", "rabbitmq", "nats", "redis-streams"],
}
def detect_category(config: dict) -> str:
"""Auto-detect category from config name and description.
Args:
config: Parsed config dictionary
Returns:
Category string (e.g., "game-engines") or "custom" if undetected
"""
name = config.get("name", "").lower()
description = config.get("description", "").lower()
text = f"{name} {description}"
best_category = "custom"
best_score = 0
for category, keywords in CATEGORY_KEYWORDS.items():
score = sum(1 for kw in keywords if kw in text)
if score > best_score:
best_score = score
best_category = category
return best_category
class ConfigPublisher:
"""Pushes validated configs to registered config source repositories."""
def __init__(self, cache_dir: str | None = None):
"""Initialize publisher.
Args:
cache_dir: Base cache directory. Defaults to ~/.skill-seekers/cache/
"""
from skill_seekers.mcp.git_repo import GitConfigRepo
self.git_repo = GitConfigRepo(cache_dir)
def publish(
self,
config_path: str | Path,
source_name: str,
category: str = "auto",
create_branch: bool = False,
force: bool = False,
) -> dict:
"""Publish a config to a registered config source repository.
Args:
config_path: Path to config JSON file
source_name: Registered source name (e.g., "spyke")
category: Category directory (e.g., "game-engines") or "auto" to detect
create_branch: Create feature branch instead of committing to main
force: Overwrite existing config if it exists
Returns:
Dict with success, config_path, commit_sha, branch, message
"""
import git
config_path = Path(config_path)
# 1. Validate config file exists
if not config_path.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
# 2. Load and validate config
with open(config_path, encoding="utf-8") as f:
config = json.load(f)
config_name = config.get("name")
if not config_name:
raise ValueError("Config JSON must have a 'name' field")
# Validate config_name to prevent path traversal
if "/" in config_name or "\\" in config_name or ".." in config_name:
raise ValueError(
f"Invalid config name '{config_name}'. "
"Path separators (/, \\) and traversal sequences (..) are not allowed."
)
try:
from skill_seekers.cli.config_validator import validate_config
validate_config(str(config_path))
logger.info(f"✅ Config validated: {config_name}")
except ValueError as e:
logger.warning(f"⚠️ Config validation warning: {e}")
# Continue — validation warnings shouldn't block push
# 3. Resolve source from registry
from skill_seekers.mcp.source_manager import SourceManager
manager = SourceManager()
source = manager.get_source(source_name)
if not source:
available = [s["name"] for s in manager.list_sources()]
raise ValueError(f"Source '{source_name}' not found. Available sources: {available}")
git_url = source["git_url"]
branch = source.get("branch", "main")
token_env = source.get("token_env")
# 4. Get token
token = os.environ.get(token_env) if token_env else None
if not token:
raise RuntimeError(
f"Token not found. Set {token_env} environment variable for source '{source_name}'"
)
# 5. Clone/pull source repo (full clone for push support)
cache_name = f"source_{source_name}"
repo_path = self.git_repo.cache_dir / cache_name
clone_url = self.git_repo.inject_token(git_url, token) if token else git_url
try:
if repo_path.exists() and (repo_path / ".git").exists():
repo_obj = git.Repo(repo_path)
repo_obj.remotes.origin.pull(branch)
logger.info(f"📥 Pulled latest from {source_name}/{branch}")
else:
repo_obj = git.Repo.clone_from(clone_url, repo_path, branch=branch)
logger.info(f"📥 Cloned {source_name} repo")
# Clear token from cached .git/config by resetting to non-token URL
repo_obj.remotes.origin.set_url(git_url)
except git.GitCommandError as e:
raise RuntimeError(f"Failed to clone/pull source repo: {e}") from e
# 6. Auto-detect category
if category == "auto":
category = detect_category(config)
logger.info(f"📂 Auto-detected category: {category}")
# 7. Check if config already exists
target_dir = repo_path / "configs" / category
target_file = target_dir / f"{config_name}.json"
if target_file.exists() and not force:
raise ValueError(
f"Config '{config_name}' already exists in {source_name}/configs/{category}/. "
"Use force=True to overwrite."
)
# 8. Copy config to target directory
target_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(config_path, target_file)
logger.info(f"📄 Placed config at configs/{category}/{config_name}.json")
# 9. Git commit and push
repo = git.Repo(repo_path)
target_branch = branch
if create_branch:
target_branch = f"config/{config_name}"
repo.git.checkout("-b", target_branch)
repo.index.add([str(target_file.relative_to(repo_path))])
action = "update" if target_file.exists() and force else "add"
commit_msg = f"feat: {action} {config_name} config in {category}"
commit = repo.index.commit(commit_msg)
# Push
try:
repo.remotes.origin.push(target_branch)
logger.info(f"🚀 Pushed to {source_name}/{target_branch}")
except git.GitCommandError as e:
raise RuntimeError(
f"Failed to push to {source_name}. Check permissions for {token_env}. Error: {e}"
) from e
# Switch back to main if we created a branch
if create_branch:
repo.git.checkout(branch)
return {
"success": True,
"config_name": config_name,
"config_path": f"configs/{category}/{config_name}.json",
"source": source_name,
"category": category,
"commit_sha": str(commit.hexsha)[:8],
"branch": target_branch,
"message": commit_msg,
}

View File

@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""
Marketplace Manager
Manages registry of plugin marketplace repositories for skill publishing.
"""
import json
from datetime import datetime, timezone
from pathlib import Path
class MarketplaceManager:
"""Manages marketplace registry at ~/.skill-seekers/marketplaces.json"""
def __init__(self, config_dir: str | None = None):
"""
Initialize marketplace manager.
Args:
config_dir: Base config directory. Defaults to ~/.skill-seekers/
"""
if config_dir:
self.config_dir = Path(config_dir)
else:
self.config_dir = Path.home() / ".skill-seekers"
self.config_dir.mkdir(parents=True, exist_ok=True)
self.registry_file = self.config_dir / "marketplaces.json"
if not self.registry_file.exists():
self._write_registry({"version": "1.0", "marketplaces": []})
def add_marketplace(
self,
name: str,
git_url: str,
token_env: str | None = None,
branch: str = "main",
author: dict | None = None,
enabled: bool = True,
) -> dict:
"""
Add or update a marketplace repository.
Args:
name: Marketplace identifier (lowercase, alphanumeric + hyphens/underscores)
git_url: Git repository URL
token_env: Environment variable name for auth token
branch: Git branch to use (default: main)
author: Default author for plugin.json ({"name": str, "email": str})
enabled: Whether marketplace is enabled (default: True)
Returns:
Marketplace dictionary
Raises:
ValueError: If name is invalid or git_url is empty
"""
if not name or not name.replace("-", "").replace("_", "").isalnum():
raise ValueError(
f"Invalid marketplace name '{name}'. "
"Must be alphanumeric with optional hyphens/underscores."
)
if not git_url or not git_url.strip():
raise ValueError("git_url cannot be empty")
if token_env is None:
token_env = self._default_token_env(git_url)
now = datetime.now(timezone.utc).isoformat()
marketplace = {
"name": name.lower(),
"git_url": git_url.strip(),
"token_env": token_env,
"branch": branch,
"author": author or {"name": "", "email": ""},
"enabled": enabled,
"added_at": now,
"updated_at": now,
}
registry = self._read_registry()
existing_index = None
for i, existing in enumerate(registry["marketplaces"]):
if existing["name"] == marketplace["name"]:
existing_index = i
marketplace["added_at"] = existing.get("added_at", marketplace["added_at"])
break
if existing_index is not None:
registry["marketplaces"][existing_index] = marketplace
else:
registry["marketplaces"].append(marketplace)
self._write_registry(registry)
return marketplace
def get_marketplace(self, name: str) -> dict:
"""
Get marketplace by name.
Raises:
KeyError: If marketplace not found
"""
registry = self._read_registry()
name_lower = name.lower()
for marketplace in registry["marketplaces"]:
if marketplace["name"] == name_lower:
return marketplace
available = [m["name"] for m in registry["marketplaces"]]
raise KeyError(
f"Marketplace '{name}' not found. "
f"Available marketplaces: {', '.join(available) if available else 'none'}"
)
def list_marketplaces(self, enabled_only: bool = False) -> list[dict]:
"""List all marketplaces."""
registry = self._read_registry()
if enabled_only:
return [m for m in registry["marketplaces"] if m.get("enabled", True)]
return registry["marketplaces"]
def remove_marketplace(self, name: str) -> bool:
"""Remove marketplace by name. Returns True if removed."""
registry = self._read_registry()
name_lower = name.lower()
for i, marketplace in enumerate(registry["marketplaces"]):
if marketplace["name"] == name_lower:
del registry["marketplaces"][i]
self._write_registry(registry)
return True
return False
def update_marketplace(self, name: str, **kwargs) -> dict:
"""
Update specific fields of an existing marketplace.
Raises:
KeyError: If marketplace not found
"""
marketplace = self.get_marketplace(name)
allowed_fields = {"git_url", "token_env", "branch", "author", "enabled"}
for field, value in kwargs.items():
if field in allowed_fields:
marketplace[field] = value
marketplace["updated_at"] = datetime.now(timezone.utc).isoformat()
registry = self._read_registry()
for i, m in enumerate(registry["marketplaces"]):
if m["name"] == marketplace["name"]:
registry["marketplaces"][i] = marketplace
break
self._write_registry(registry)
return marketplace
def _read_registry(self) -> dict:
"""Read registry from file."""
try:
with open(self.registry_file, encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError as e:
raise ValueError(f"Corrupted registry file: {e}") from e
def _write_registry(self, registry: dict) -> None:
"""Write registry to file atomically."""
if "version" not in registry or "marketplaces" not in registry:
raise ValueError("Invalid registry schema")
temp_file = self.registry_file.with_suffix(".tmp")
try:
with open(temp_file, "w", encoding="utf-8") as f:
json.dump(registry, f, indent=2, ensure_ascii=False)
temp_file.replace(self.registry_file)
except Exception as e:
if temp_file.exists():
temp_file.unlink()
raise e
@staticmethod
def _default_token_env(git_url: str) -> str:
"""Get default token environment variable name from git URL."""
url_lower = git_url.lower()
if "github" in url_lower:
return "GITHUB_TOKEN"
elif "gitlab" in url_lower:
return "GITLAB_TOKEN"
elif "bitbucket" in url_lower:
return "BITBUCKET_TOKEN"
elif "gitea" in url_lower:
return "GITEA_TOKEN"
return "GIT_TOKEN"

View File

@@ -0,0 +1,291 @@
#!/usr/bin/env python3
"""
Marketplace Publisher
Publishes packaged skills to Claude Code plugin marketplace repositories.
"""
import json
import logging
import os
import re
import shutil
from pathlib import Path
import yaml
from skill_seekers.mcp.marketplace_manager import MarketplaceManager
logger = logging.getLogger(__name__)
class MarketplacePublisher:
"""Publishes skills to registered plugin marketplace repositories."""
def __init__(self, cache_dir: str | None = None):
"""
Initialize publisher.
Args:
cache_dir: Base cache directory. Defaults to ~/.skill-seekers/cache/
"""
from skill_seekers.mcp.git_repo import GitConfigRepo
self.git_repo = GitConfigRepo(cache_dir)
def publish(
self,
skill_dir: str | Path,
marketplace_name: str,
category: str = "development",
skill_name: str | None = None,
description: str | None = None,
create_branch: bool = False,
force: bool = False,
) -> dict:
"""
Publish a skill to a plugin marketplace repository.
Args:
skill_dir: Path to skill directory (contains SKILL.md + references/)
marketplace_name: Registered marketplace name
category: Plugin category in marketplace (default: "development")
skill_name: Override skill name (defaults to SKILL.md frontmatter or dir name)
description: Override description (defaults to SKILL.md frontmatter)
create_branch: Create feature branch instead of committing to main
force: Overwrite existing plugin if it exists
Returns:
Dict with success, plugin_path, commit_sha, branch, message
"""
import git
skill_dir = Path(skill_dir)
# 1. Validate skill directory
skill_md_path = skill_dir / "SKILL.md"
if not skill_md_path.exists():
raise FileNotFoundError(f"SKILL.md not found in {skill_dir}")
# 2. Read frontmatter for metadata
frontmatter = self._read_frontmatter(skill_md_path)
if not skill_name:
skill_name = frontmatter.get("name") or skill_dir.name
if not description:
description = frontmatter.get("description", f"Skill for {skill_name}")
# 2b. Validate skill_name to prevent path traversal
skill_name = self._validate_skill_name(skill_name)
# 3. Resolve marketplace
manager = MarketplaceManager()
marketplace = manager.get_marketplace(marketplace_name)
git_url = marketplace["git_url"]
branch = marketplace["branch"]
token_env = marketplace["token_env"]
author = marketplace.get("author", {"name": "", "email": ""})
# 4. Get token
token = os.environ.get(token_env) if token_env else None
if not token:
raise RuntimeError(
f"Token not found. Set {token_env} environment variable "
f"for marketplace '{marketplace_name}'"
)
# 5. Clone/pull marketplace repo (full clone, not shallow — needed for push)
cache_name = f"marketplace_{marketplace_name}"
repo_path = self.git_repo.cache_dir / cache_name
clone_url = self.git_repo.inject_token(git_url, token) if token else git_url
try:
if repo_path.exists() and (repo_path / ".git").exists():
repo_obj = git.Repo(repo_path)
repo_obj.remotes.origin.pull(branch)
else:
repo_obj = git.Repo.clone_from(clone_url, repo_path, branch=branch)
# Clear token from cached .git/config by resetting to non-token URL
repo_obj.remotes.origin.set_url(git_url)
except git.GitCommandError as e:
raise RuntimeError(f"Failed to clone/pull marketplace repo: {e}") from e
# 6. Check for existing plugin
plugin_dir = repo_path / "plugins" / skill_name
if plugin_dir.exists() and not force:
raise ValueError(
f"Plugin '{skill_name}' already exists in marketplace '{marketplace_name}'. "
"Use force=True to overwrite."
)
# 7. Create plugin directory structure + commit + push
# Wrap in try/finally to clean up partial plugin dir on failure
plugin_created = False
try:
self._copy_skill_to_plugin(skill_dir, plugin_dir, skill_name)
plugin_created = True
# 8. Generate plugin.json
plugin_json = self._generate_plugin_json(skill_name, description, author)
plugin_json_dir = plugin_dir / ".claude-plugin"
plugin_json_dir.mkdir(parents=True, exist_ok=True)
with open(plugin_json_dir / "plugin.json", "w", encoding="utf-8") as f:
json.dump(plugin_json, f, indent=2, ensure_ascii=False)
# 9. Update marketplace.json
self._update_marketplace_json(repo_path, skill_name, description, author, category)
# 10. Git commit and push
repo = git.Repo(repo_path)
target_branch = branch
if create_branch:
target_branch = f"skill/{skill_name}"
repo.git.checkout("-b", target_branch)
# Only stage the specific files we wrote (not the entire repo)
files_to_stage = []
# Stage the plugin directory (skill files + plugin.json)
plugin_rel = str(plugin_dir.relative_to(repo_path))
files_to_stage.append(plugin_rel)
# Stage marketplace.json
marketplace_json_rel = str(
(repo_path / ".claude-plugin" / "marketplace.json").relative_to(repo_path)
)
files_to_stage.append(marketplace_json_rel)
repo.index.add(files_to_stage)
action = "update" if force else "add"
commit_msg = f"feat: {action} {skill_name} skill plugin"
repo.index.commit(commit_msg)
commit_sha = repo.head.commit.hexsha[:7]
push_url = self.git_repo.inject_token(git_url, token)
repo.git.push(push_url, target_branch)
return {
"success": True,
"plugin_path": f"plugins/{skill_name}",
"commit_sha": commit_sha,
"branch": target_branch,
"message": f"Published '{skill_name}' to marketplace '{marketplace_name}'",
}
except git.GitCommandError as e:
# Reset git state on push/commit failure
try:
repo = git.Repo(repo_path)
repo.git.checkout("--", ".")
repo.git.clean("-fd", "plugins/" + skill_name)
except Exception:
pass
raise RuntimeError(f"Git operation failed: {e}") from e
except Exception:
# Clean up partial plugin directory on non-git failure
if plugin_created and plugin_dir.exists():
shutil.rmtree(plugin_dir, ignore_errors=True)
raise
@staticmethod
def _validate_skill_name(name: str) -> str:
"""
Validate skill name to prevent path traversal and injection.
Args:
name: Skill name to validate
Returns:
Validated skill name
Raises:
ValueError: If name contains invalid characters
"""
if not name or not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$", name):
raise ValueError(
f"Invalid skill name '{name}'. "
"Must start with alphanumeric and contain only alphanumeric, hyphens, underscores, or dots."
)
if ".." in name or "/" in name or "\\" in name:
raise ValueError(f"Invalid skill name '{name}'. Path traversal characters not allowed.")
return name
def _read_frontmatter(self, skill_md_path: Path) -> dict:
"""Parse YAML frontmatter from SKILL.md."""
content = skill_md_path.read_text(encoding="utf-8")
if not content.startswith("---"):
return {}
parts = content.split("---", 2)
if len(parts) < 3:
return {}
try:
return yaml.safe_load(parts[1]) or {}
except yaml.YAMLError:
return {}
def _copy_skill_to_plugin(self, skill_dir: Path, plugin_dir: Path, skill_name: str) -> None:
"""Copy skill files into plugin directory structure."""
skills_dest = plugin_dir / "skills" / skill_name
if skills_dest.exists():
shutil.rmtree(skills_dest)
skills_dest.mkdir(parents=True, exist_ok=True)
shutil.copy2(skill_dir / "SKILL.md", skills_dest / "SKILL.md")
refs_src = skill_dir / "references"
if refs_src.exists() and refs_src.is_dir():
shutil.copytree(refs_src, skills_dest / "references", dirs_exist_ok=True)
def _generate_plugin_json(self, skill_name: str, description: str, author: dict) -> dict:
"""Generate plugin.json content."""
return {
"name": skill_name,
"description": description,
"author": author,
}
def _update_marketplace_json(
self,
repo_path: Path,
skill_name: str,
description: str,
author: dict,
category: str,
) -> None:
"""Update root .claude-plugin/marketplace.json with new plugin entry."""
marketplace_json_path = repo_path / ".claude-plugin" / "marketplace.json"
if marketplace_json_path.exists():
with open(marketplace_json_path, encoding="utf-8") as f:
data = json.load(f)
else:
marketplace_json_path.parent.mkdir(parents=True, exist_ok=True)
data = {
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": repo_path.name,
"description": "",
"owner": author,
"plugins": [],
}
entry = {
"name": skill_name,
"description": description,
"author": author,
"source": f"./plugins/{skill_name}",
"category": category,
}
updated = False
for i, plugin in enumerate(data["plugins"]):
if plugin["name"] == skill_name:
data["plugins"][i] = entry
updated = True
break
if not updated:
data["plugins"].append(entry)
data["plugins"].sort(key=lambda p: p["name"])
with open(marketplace_json_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)

View File

@@ -3,7 +3,7 @@
Skill Seeker MCP Server (FastMCP Implementation)
Modern, decorator-based MCP server using FastMCP for simplified tool registration.
Provides 34 tools for generating Claude AI skills from documentation.
Provides 34 tools for generating LLM skills from documentation.
This is a streamlined alternative to server.py (2200 lines → 708 lines, 68% reduction).
All tool implementations are delegated to modular tool files in tools/ directory.
@@ -15,7 +15,8 @@ All tool implementations are delegated to modular tool files in tools/ directory
* Scraping tools (11): estimate_pages, scrape_docs, scrape_github, scrape_pdf, scrape_video, scrape_codebase, detect_patterns, extract_test_examples, build_how_to_guides, extract_config_patterns, scrape_generic
* Packaging tools (4): package_skill, upload_skill, enhance_skill, install_skill
* Splitting tools (2): split_config, generate_router
* Source tools (5): fetch_config, submit_config, add_config_source, list_config_sources, remove_config_source
* Source tools (6): fetch_config, submit_config, push_config, add_config_source, list_config_sources, remove_config_source
* Marketplace tools (4): add_marketplace, list_marketplaces, remove_marketplace, publish_to_marketplace
* Vector Database tools (4): export_to_weaviate, export_to_chroma, export_to_faiss, export_to_qdrant
* Workflow tools (5): list_workflows, get_workflow, create_workflow, update_workflow, delete_workflow
@@ -86,6 +87,11 @@ try:
extract_test_examples_impl,
# Source tools
fetch_config_impl,
# Marketplace tools
add_marketplace_impl,
list_marketplaces_impl,
remove_marketplace_impl,
publish_to_marketplace_impl,
# Config tools
generate_config_impl,
generate_router_impl,
@@ -133,6 +139,10 @@ except ImportError:
extract_config_patterns_impl,
extract_test_examples_impl,
fetch_config_impl,
add_marketplace_impl,
list_marketplaces_impl,
remove_marketplace_impl,
publish_to_marketplace_impl,
generate_config_impl,
generate_router_impl,
install_skill_impl,
@@ -163,7 +173,7 @@ mcp = None
if MCP_AVAILABLE and FastMCP is not None:
mcp = FastMCP(
name="skill-seeker",
instructions="Skill Seeker MCP Server - Generate Claude AI skills from documentation",
instructions="Skill Seeker MCP Server - Generate LLM skills from documentation",
)
@@ -338,7 +348,7 @@ async def estimate_pages(
@safe_tool_decorator(
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."
description="Scrape documentation and build LLM 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."
)
async def scrape_docs(
config_path: str,
@@ -349,12 +359,12 @@ async def scrape_docs(
merge_mode: str | None = None,
) -> str:
"""
Scrape documentation and build Claude skill.
Scrape documentation and build LLM skill.
Args:
config_path: Path to config JSON file (e.g., configs/react.json or configs/godot_unified.json)
unlimited: Remove page limit - scrape all pages (default: false). Overrides max_pages in config.
enhance_local: Open terminal for local enhancement with Claude Code (default: false)
enhance_local: Open terminal for local enhancement with AI coding agent (default: false)
skip_scrape: Skip scraping, use cached data (default: false)
dry_run: Preview what will be scraped without saving (default: false)
merge_mode: Override merge mode for unified configs: 'rule-based' or 'claude-enhanced' (default: from config)
@@ -879,7 +889,7 @@ async def scrape_generic(
)
async def package_skill(
skill_dir: str,
target: str = "claude",
target: str = "auto",
auto_upload: bool = True,
) -> str:
"""
@@ -887,12 +897,16 @@ async def package_skill(
Args:
skill_dir: Path to skill directory to package (e.g., output/react/)
target: Target platform (default: 'claude'). Options: claude, gemini, openai, markdown
target: Target platform (default: 'auto'). Options: auto, claude, gemini, openai, markdown
auto_upload: Auto-upload after packaging if API key is available (default: true). Requires platform-specific API key: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY.
Returns:
Packaging results with file path and platform info.
"""
if target == "auto":
from skill_seekers.cli.agent_client import AgentClient
target = AgentClient.detect_default_target()
args = {
"skill_dir": skill_dir,
"target": target,
@@ -909,7 +923,7 @@ async def package_skill(
)
async def upload_skill(
skill_zip: str,
target: str = "claude",
target: str = "auto",
api_key: str | None = None,
) -> str:
"""
@@ -917,12 +931,16 @@ async def upload_skill(
Args:
skill_zip: Path to skill package (.zip or .tar.gz, e.g., output/react.zip)
target: Target platform (default: 'claude'). Options: claude, gemini, openai
target: Target platform (default: 'auto'). Options: auto, claude, gemini, openai
api_key: Optional API key (uses env var if not provided: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY)
Returns:
Upload results with skill ID and platform URL.
"""
if target == "auto":
from skill_seekers.cli.agent_client import AgentClient
target = AgentClient.detect_default_target()
args = {
"skill_zip": skill_zip,
"target": target,
@@ -937,11 +955,11 @@ async def upload_skill(
@safe_tool_decorator(
description="Enhance SKILL.md with AI using target platform's model. Local mode uses Claude Code Max (no API key). API mode uses platform API (requires key). Transforms basic templates into comprehensive 500+ line guides with examples."
description="Enhance SKILL.md with AI using target platform's model. Local mode uses AI coding agent (no API key). API mode uses platform API (requires key). Transforms basic templates into comprehensive 500+ line guides with examples."
)
async def enhance_skill(
skill_dir: str,
target: str = "claude",
target: str = "auto",
mode: str = "local",
api_key: str | None = None,
) -> str:
@@ -950,13 +968,17 @@ async def enhance_skill(
Args:
skill_dir: Path to skill directory containing SKILL.md (e.g., output/react/)
target: Target platform (default: 'claude'). Options: claude, gemini, openai
mode: Enhancement mode (default: 'local'). Options: local (Claude Code, no API), api (uses platform API)
target: Target platform (default: 'auto'). Options: auto, claude, gemini, openai
mode: Enhancement mode (default: 'local'). Options: local (AI coding agent, no API), api (uses platform API)
api_key: Optional API key for 'api' mode (uses env var if not provided: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY)
Returns:
Enhancement results with backup location.
"""
if target == "auto":
from skill_seekers.cli.agent_client import AgentClient
target = AgentClient.detect_default_target()
args = {
"skill_dir": skill_dir,
"target": target,
@@ -972,7 +994,7 @@ async def enhance_skill(
@safe_tool_decorator(
description="Complete one-command workflow: fetch config → scrape docs → AI enhance (MANDATORY) → package → upload. Enhancement required for quality (3/10→9/10). Takes 20-45 min depending on config size. Supports multiple LLM platforms: claude (default), gemini, openai, markdown. Auto-uploads if platform API key is set."
description="Complete one-command workflow: fetch config → scrape docs → AI enhance (MANDATORY) → package → upload. Enhancement required for quality (3/10→9/10). Takes 20-45 min depending on config size. Supports multiple LLM platforms: auto (detects from environment), claude, gemini, openai, markdown. Auto-uploads if platform API key is set."
)
async def install_skill(
config_name: str | None = None,
@@ -981,7 +1003,7 @@ async def install_skill(
auto_upload: bool = True,
unlimited: bool = False,
dry_run: bool = False,
target: str = "claude",
target: str = "auto",
) -> str:
"""
Complete one-command workflow to install a skill.
@@ -993,11 +1015,15 @@ async def install_skill(
auto_upload: Auto-upload after packaging (requires platform API key). Default: true. Set to false to skip upload.
unlimited: Remove page limits during scraping (default: false). WARNING: Can take hours for large sites.
dry_run: Preview workflow without executing (default: false). Shows all phases that would run.
target: Target LLM platform (default: 'claude'). Options: claude, gemini, openai, markdown. Requires corresponding API key: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY.
target: Target LLM platform (default: 'auto'). Options: auto, claude, gemini, openai, markdown. Requires corresponding API key: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY.
Returns:
Workflow results with all phase statuses.
"""
if target == "auto":
from skill_seekers.cli.agent_client import AgentClient
target = AgentClient.detect_default_target()
args = {
"destination": destination,
"auto_upload": auto_upload,
@@ -1263,6 +1289,150 @@ async def remove_config_source(name: str) -> str:
return str(result)
@safe_tool_decorator(
description="Push a config to a registered config source repository. Validates, places in category directory, commits, and pushes."
)
async def push_config(
config_path: str,
source_name: str,
category: str = "auto",
create_branch: bool = False,
force: bool = False,
) -> str:
"""
Push a config to a registered config source repository.
Args:
config_path: Path to config JSON file. Example: 'configs/unity-spine.json'
source_name: Registered source name. Example: 'spyke'
category: Category directory (e.g., 'game-engines'). Auto-detected if 'auto'.
create_branch: Create feature branch instead of pushing to main. Default: false
force: Overwrite existing config if it exists. Default: false
Returns:
Push results with commit SHA and config location.
"""
from skill_seekers.mcp.tools.source_tools import push_config_tool
result = await push_config_tool(
{
"config_path": config_path,
"source_name": source_name,
"category": category,
"create_branch": create_branch,
"force": force,
}
)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
# ============================================================================
# MARKETPLACE TOOLS (4 tools)
# ============================================================================
@safe_tool_decorator(
description="Register a plugin marketplace repository. Allows publishing skills to private/team plugin repos. Supports GitHub, GitLab, Bitbucket with per-repo authentication."
)
async def add_marketplace(
name: str,
git_url: str,
token_env: str = None,
branch: str = "main",
author_name: str = "",
author_email: str = "",
enabled: bool = True,
) -> str:
"""
Register a plugin marketplace repository.
Args:
name: Marketplace identifier (lowercase, alphanumeric + hyphens/underscores). Example: 'spyke'
git_url: Git repository URL. Example: 'https://github.com/myorg/plugins.git'
token_env: Environment variable name for auth token (auto-detected from URL). Example: 'GITHUB_TOKEN'
branch: Git branch to use (default: "main")
author_name: Default author name for generated plugin.json files
author_email: Default author email for generated plugin.json files
enabled: Whether marketplace is enabled (default: true)
"""
result = await add_marketplace_impl(
{
"name": name,
"git_url": git_url,
"token_env": token_env,
"branch": branch,
"author_name": author_name,
"author_email": author_email,
"enabled": enabled,
}
)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(description="List all registered plugin marketplace repositories.")
async def list_marketplaces(enabled_only: bool = False) -> str:
"""List all registered plugin marketplace repositories."""
result = await list_marketplaces_impl({"enabled_only": enabled_only})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Remove a registered plugin marketplace. Deletes from registry but not cached data."
)
async def remove_marketplace(name: str) -> str:
"""Remove a registered plugin marketplace."""
result = await remove_marketplace_impl({"name": name})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Publish a packaged skill to a plugin marketplace repository. Creates a Claude Code plugin in the target marketplace repo."
)
async def publish_to_marketplace(
skill_dir: str,
marketplace: str,
category: str = "development",
skill_name: str = None,
description: str = None,
create_branch: bool = False,
force: bool = False,
) -> str:
"""
Publish a skill to a plugin marketplace repository.
Args:
skill_dir: Path to skill directory containing SKILL.md. Example: 'output/react/'
marketplace: Registered marketplace name. Example: 'spyke'
category: Plugin category (default: "development")
skill_name: Override skill name (optional)
description: Override description (optional)
create_branch: Create feature branch instead of committing to main (default: false)
force: Overwrite existing plugin (default: false)
"""
result = await publish_to_marketplace_impl(
{
"skill_dir": skill_dir,
"marketplace": marketplace,
"category": category,
"skill_name": skill_name,
"description": description,
"create_branch": create_branch,
"force": force,
}
)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
# ============================================================================
# VECTOR DATABASE TOOLS (4 tools)
# ============================================================================

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Skill Seeker MCP Server
Model Context Protocol server for generating Claude AI skills from documentation
Model Context Protocol server for generating LLM skills from documentation
"""
import asyncio
@@ -200,7 +200,7 @@ async def list_tools() -> list[Tool]:
),
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.",
description="Scrape documentation and build LLM 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": {
@@ -215,7 +215,7 @@ async def list_tools() -> list[Tool]:
},
"enhance_local": {
"type": "boolean",
"description": "Open terminal for local enhancement with Claude Code (default: false)",
"description": "Open terminal for local enhancement with AI coding agent (default: false)",
"default": False,
},
"skip_scrape": {
@@ -230,7 +230,7 @@ async def list_tools() -> list[Tool]:
},
"merge_mode": {
"type": "string",
"description": "Override merge mode for unified configs: 'rule-based' or 'claude-enhanced' (default: from config)",
"description": "Override merge mode for unified configs: 'rule-based' or 'ai-enhanced' (default: from config)",
},
},
"required": ["config_path"],
@@ -238,7 +238,7 @@ async def list_tools() -> list[Tool]:
),
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.",
description="Package a skill directory into a .zip file ready for upload. Automatically uploads if ANTHROPIC_API_KEY is set.",
inputSchema={
"type": "object",
"properties": {
@@ -257,7 +257,7 @@ async def list_tools() -> list[Tool]:
),
Tool(
name="upload_skill",
description="Upload a skill .zip file to Claude automatically (requires ANTHROPIC_API_KEY)",
description="Upload a skill .zip file automatically (requires ANTHROPIC_API_KEY)",
inputSchema={
"type": "object",
"properties": {
@@ -340,7 +340,7 @@ async def list_tools() -> list[Tool]:
),
Tool(
name="scrape_pdf",
description="Scrape PDF documentation and build Claude skill. Extracts text, code, and images from PDF files.",
description="Scrape PDF documentation and build LLM skill. Extracts text, code, and images from PDF files.",
inputSchema={
"type": "object",
"properties": {
@@ -370,7 +370,7 @@ async def list_tools() -> list[Tool]:
),
Tool(
name="scrape_github",
description="Scrape GitHub repository and build Claude skill. Extracts README, Issues, Changelog, Releases, and code structure.",
description="Scrape GitHub repository and build LLM skill. Extracts README, Issues, Changelog, Releases, and code structure.",
inputSchema={
"type": "object",
"properties": {
@@ -425,7 +425,7 @@ async def list_tools() -> list[Tool]:
),
Tool(
name="install_skill",
description="Complete one-command workflow: fetch config → scrape docs → AI enhance (MANDATORY) → package → upload. Enhancement required for quality (3/10→9/10). Takes 20-45 min depending on config size. Automatically uploads to Claude if ANTHROPIC_API_KEY is set.",
description="Complete one-command workflow: fetch config → scrape docs → AI enhance (MANDATORY) → package → upload. Enhancement required for quality (3/10→9/10). Takes 20-45 min depending on config size. Automatically uploads if ANTHROPIC_API_KEY is set.",
inputSchema={
"type": "object",
"properties": {
@@ -444,7 +444,7 @@ async def list_tools() -> list[Tool]:
},
"auto_upload": {
"type": "boolean",
"description": "Auto-upload to Claude after packaging (requires ANTHROPIC_API_KEY). Default: true. Set to false to skip upload.",
"description": "Auto-upload after packaging (requires ANTHROPIC_API_KEY). Default: true. Set to false to skip upload.",
"default": True,
},
"unlimited": {
@@ -898,7 +898,7 @@ async def package_skill_tool(args: dict) -> list[TextContent]:
if should_upload:
# Upload succeeded
output += "\n\n✅ Skill packaged and uploaded automatically!"
output += "\n Your skill is now available in Claude!"
output += "\n Your skill has been uploaded successfully!"
elif auto_upload and not has_api_key:
# User wanted upload but no API key
output += "\n\n📝 Skill packaged successfully!"
@@ -922,7 +922,7 @@ async def package_skill_tool(args: dict) -> list[TextContent]:
async def upload_skill_tool(args: dict) -> list[TextContent]:
"""Upload skill .zip to Claude"""
"""Upload skill .zip file"""
skill_zip = args["skill_zip"]
# Run upload_skill.py
@@ -931,7 +931,7 @@ async def upload_skill_tool(args: dict) -> list[TextContent]:
# Timeout: 5 minutes for upload
timeout = 300
progress_msg = "📤 Uploading skill to Claude...\n"
progress_msg = "📤 Uploading skill...\n"
progress_msg += f"⏱️ Maximum time: {timeout // 60} minutes\n\n"
stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
@@ -1194,7 +1194,7 @@ async def scrape_pdf_tool(args: dict) -> list[TextContent]:
async def scrape_github_tool(args: dict) -> list[TextContent]:
"""Scrape GitHub repository to Claude skill (C1.11)"""
"""Scrape GitHub repository to LLM skill (C1.11)"""
repo = args.get("repo")
config_path = args.get("config_path")
name = args.get("name")
@@ -1550,7 +1550,7 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
2. Scrape documentation
3. AI Enhancement (MANDATORY - no skip option)
4. Package to .zip
5. Upload to Claude (optional)
5. Upload (optional)
Args:
config_name: Config to fetch from API (mutually exclusive with config_path)
@@ -1743,7 +1743,7 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
output_lines.append(stdout)
workflow_state["phases_completed"].append("enhance_skill")
else:
output_lines.append(" [DRY RUN] Would enhance SKILL.md with Claude Code")
output_lines.append(" [DRY RUN] Would enhance SKILL.md with AI agent")
output_lines.append("")
@@ -1786,7 +1786,7 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
# ===== PHASE 5: Upload (Optional) =====
if auto_upload:
phase_num = "5/5" if config_name else "4/4"
output_lines.append(f"📤 PHASE {phase_num}: Upload to Claude")
output_lines.append(f"📤 PHASE {phase_num}: Upload Skill")
output_lines.append("-" * 70)
output_lines.append(f"Zip file: {workflow_state['zip_path']}")
output_lines.append("")
@@ -1817,7 +1817,7 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
output_lines.append(" 2. Click 'Upload Skill'")
output_lines.append(f" 3. Select: {workflow_state['zip_path']}")
else:
output_lines.append(" [DRY RUN] Would upload to Claude (if API key set)")
output_lines.append(" [DRY RUN] Would upload skill (if API key set)")
output_lines.append("")
@@ -1840,7 +1840,7 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
output_lines.append("")
if auto_upload and has_api_key:
output_lines.append("🎉 Your skill is now available in Claude!")
output_lines.append("🎉 Your skill has been uploaded successfully!")
output_lines.append(" Go to https://claude.ai/skills to use it")
elif auto_upload:
output_lines.append("📝 Manual upload required (see instructions above)")

View File

@@ -9,6 +9,7 @@ Tools are organized by functionality:
- packaging_tools: Skill packaging and upload
- splitting_tools: Config splitting and router generation
- source_tools: Config source management (fetch, submit, add/remove sources)
- marketplace_tools: Marketplace management (add, list, remove, publish)
- vector_db_tools: Vector database export (Weaviate, Chroma, FAISS, Qdrant)
"""
@@ -84,6 +85,18 @@ from .source_tools import (
from .source_tools import (
submit_config_tool as submit_config_impl,
)
from .marketplace_tools import (
add_marketplace_tool as add_marketplace_impl,
)
from .marketplace_tools import (
list_marketplaces_tool as list_marketplaces_impl,
)
from .marketplace_tools import (
publish_to_marketplace_tool as publish_to_marketplace_impl,
)
from .marketplace_tools import (
remove_marketplace_tool as remove_marketplace_impl,
)
from .splitting_tools import (
generate_router as generate_router_impl,
)
@@ -147,6 +160,11 @@ __all__ = [
# Splitting tools
"split_config_impl",
"generate_router_impl",
# Marketplace tools
"add_marketplace_impl",
"list_marketplaces_impl",
"remove_marketplace_impl",
"publish_to_marketplace_impl",
# Source tools
"fetch_config_impl",
"submit_config_impl",

View File

@@ -0,0 +1,226 @@
"""
Marketplace management tools for MCP server.
This module contains tools for managing plugin marketplace repositories:
- add_marketplace: Register a plugin marketplace repository
- list_marketplaces: List all registered marketplace repositories
- remove_marketplace: Remove a registered marketplace
- publish_to_marketplace: Publish a packaged skill to a marketplace
"""
# MCP types (imported conditionally)
try:
from mcp.types import TextContent
MCP_AVAILABLE = True
except ImportError:
class TextContent:
"""Fallback TextContent for when MCP is not installed"""
def __init__(self, type: str, text: str):
self.type = type
self.text = text
MCP_AVAILABLE = False
async def add_marketplace_tool(args: dict) -> list[TextContent]:
"""Register a plugin marketplace repository."""
from skill_seekers.mcp.marketplace_manager import MarketplaceManager
name = args.get("name")
git_url = args.get("git_url")
token_env = args.get("token_env")
branch = args.get("branch", "main")
author_name = args.get("author_name", "")
author_email = args.get("author_email", "")
enabled = args.get("enabled", True)
try:
if not name:
return [TextContent(type="text", text="❌ Error: 'name' parameter is required")]
if not git_url:
return [TextContent(type="text", text="❌ Error: 'git_url' parameter is required")]
author = {"name": author_name, "email": author_email}
manager = MarketplaceManager()
marketplace = manager.add_marketplace(
name=name,
git_url=git_url,
token_env=token_env,
branch=branch,
author=author,
enabled=enabled,
)
is_update = marketplace["added_at"] != marketplace["updated_at"]
result = f"""✅ Marketplace {"updated" if is_update else "registered"} successfully!
📛 Name: {marketplace["name"]}
📁 Repository: {marketplace["git_url"]}
🌿 Branch: {marketplace["branch"]}
🔑 Token env: {marketplace["token_env"]}
👤 Author: {marketplace["author"]["name"]} <{marketplace["author"]["email"]}>
✓ Enabled: {marketplace["enabled"]}
🕒 Added: {marketplace["added_at"][:19]}
Usage:
# Publish a skill to this marketplace
publish_to_marketplace(skill_dir="output/my-skill", marketplace="{marketplace["name"]}")
# List all marketplaces
list_marketplaces()
💡 Set {marketplace["token_env"]} environment variable for private repos
"""
return [TextContent(type="text", text=result)]
except ValueError as e:
return [TextContent(type="text", text=f"❌ Validation Error: {str(e)}")]
except Exception as e:
return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
async def list_marketplaces_tool(args: dict) -> list[TextContent]:
"""List all registered plugin marketplace repositories."""
from skill_seekers.mcp.marketplace_manager import MarketplaceManager
enabled_only = args.get("enabled_only", False)
try:
manager = MarketplaceManager()
marketplaces = manager.list_marketplaces(enabled_only=enabled_only)
if not marketplaces:
result = """📋 No marketplaces registered
To add a marketplace:
add_marketplace(
name="my-plugins",
git_url="https://github.com/myorg/plugins.git",
author_name="My Team",
author_email="team@example.com"
)
💡 Once added, use: publish_to_marketplace(skill_dir="...", marketplace="my-plugins")
"""
return [TextContent(type="text", text=result)]
result = f"📋 Plugin Marketplaces ({len(marketplaces)} total"
if enabled_only:
result += ", enabled only"
result += ")\n\n"
for mp in marketplaces:
status_icon = "" if mp.get("enabled", True) else ""
author = mp.get("author", {})
author_str = f"{author.get('name', '')} <{author.get('email', '')}>"
result += f"{status_icon} **{mp['name']}**\n"
result += f" 📁 {mp['git_url']}\n"
result += f" 🌿 Branch: {mp['branch']} | 🔑 Token: {mp['token_env']}\n"
result += f" 👤 Author: {author_str}\n"
result += f" 🕒 Added: {mp['added_at'][:19]}\n"
result += "\n"
result += """Usage:
# Publish skill to a marketplace
publish_to_marketplace(skill_dir="output/my-skill", marketplace="MARKETPLACE_NAME")
# Add new marketplace
add_marketplace(name="...", git_url="...")
# Remove marketplace
remove_marketplace(name="MARKETPLACE_NAME")
"""
return [TextContent(type="text", text=result)]
except Exception as e:
return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
async def remove_marketplace_tool(args: dict) -> list[TextContent]:
"""Remove a registered plugin marketplace."""
from skill_seekers.mcp.marketplace_manager import MarketplaceManager
name = args.get("name")
try:
if not name:
return [TextContent(type="text", text="❌ Error: 'name' parameter is required")]
manager = MarketplaceManager()
removed = manager.remove_marketplace(name)
if removed:
result = f"""✅ Marketplace removed successfully!
📛 Removed: {name}
⚠️ Note: Cached repository data is NOT deleted
To free disk space, manually delete: ~/.skill-seekers/cache/marketplace_{name}/
"""
return [TextContent(type="text", text=result)]
else:
sources = manager.list_marketplaces()
available = [m["name"] for m in sources]
result = f"""❌ Marketplace '{name}' not found
Available marketplaces: {", ".join(available) if available else "none"}
"""
return [TextContent(type="text", text=result)]
except Exception as e:
return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
async def publish_to_marketplace_tool(args: dict) -> list[TextContent]:
"""Publish a packaged skill to a plugin marketplace repository."""
from skill_seekers.mcp.marketplace_publisher import MarketplacePublisher
skill_dir = args.get("skill_dir")
marketplace = args.get("marketplace")
category = args.get("category", "development")
skill_name = args.get("skill_name")
description = args.get("description")
create_branch = args.get("create_branch", False)
force = args.get("force", False)
try:
if not skill_dir:
return [TextContent(type="text", text="❌ Error: 'skill_dir' parameter is required")]
if not marketplace:
return [TextContent(type="text", text="❌ Error: 'marketplace' parameter is required")]
publisher = MarketplacePublisher()
result = publisher.publish(
skill_dir=skill_dir,
marketplace_name=marketplace,
category=category,
skill_name=skill_name,
description=description,
create_branch=create_branch,
force=force,
)
if result["success"]:
output = f"""✅ Skill published to marketplace successfully!
📦 Plugin: {result["plugin_path"]}
🏪 Marketplace: {marketplace}
🏷️ Category: {category}
🌿 Branch: {result["branch"]}
📝 Commit: {result["commit_sha"]}
{result["message"]}
"""
return [TextContent(type="text", text=output)]
else:
return [TextContent(type="text", text=f"❌ Publish failed: {result['message']}")]
except (FileNotFoundError, KeyError, ValueError, RuntimeError) as e:
return [TextContent(type="text", text=f"{str(e)}")]
except Exception as e:
return [TextContent(type="text", text=f"❌ Error: {str(e)}")]

View File

@@ -113,8 +113,8 @@ async def package_skill_tool(args: dict) -> list[TextContent]:
args: Dictionary with:
- skill_dir (str): Path to skill directory (e.g., output/react/)
- auto_upload (bool): Try to upload automatically if API key is available (default: True)
- target (str): Target platform (default: 'claude')
Options: 'claude', 'gemini', 'openai', 'markdown'
- target (str): Target platform (default: 'auto')
Options: 'auto', 'claude', 'gemini', 'openai', 'markdown'
Returns:
List of TextContent with packaging results
@@ -123,7 +123,11 @@ async def package_skill_tool(args: dict) -> list[TextContent]:
skill_dir = args["skill_dir"]
auto_upload = args.get("auto_upload", True)
target = args.get("target", "claude")
target = args.get("target", "auto")
if target == "auto":
from skill_seekers.cli.agent_client import AgentClient
target = AgentClient.detect_default_target()
# Get platform adaptor
try:
@@ -232,8 +236,8 @@ async def upload_skill_tool(args: dict) -> list[TextContent]:
Args:
args: Dictionary with:
- skill_zip (str): Path to skill package (.zip or .tar.gz)
- target (str): Target platform (default: 'claude')
Options: 'claude', 'gemini', 'openai'
- target (str): Target platform (default: 'auto')
Options: 'auto', 'claude', 'gemini', 'openai'
Note: 'markdown' does not support upload
- api_key (str, optional): API key (uses env var if not provided)
@@ -243,7 +247,11 @@ async def upload_skill_tool(args: dict) -> list[TextContent]:
from skill_seekers.cli.adaptors import get_adaptor
skill_zip = args["skill_zip"]
target = args.get("target", "claude")
target = args.get("target", "auto")
if target == "auto":
from skill_seekers.cli.agent_client import AgentClient
target = AgentClient.detect_default_target()
api_key = args.get("api_key")
# Get platform adaptor
@@ -296,11 +304,11 @@ async def enhance_skill_tool(args: dict) -> list[TextContent]:
Args:
args: Dictionary with:
- skill_dir (str): Path to skill directory
- target (str): Target platform (default: 'claude')
Options: 'claude', 'gemini', 'openai'
- target (str): Target platform (default: 'auto')
Options: 'auto', 'claude', 'gemini', 'openai'
Note: 'markdown' does not support enhancement
- mode (str): Enhancement mode (default: 'local')
'local': Uses Claude Code Max (no API key)
'local': Uses AI coding agent (no API key)
'api': Uses platform API (requires API key)
- api_key (str, optional): API key for 'api' mode
@@ -310,7 +318,11 @@ async def enhance_skill_tool(args: dict) -> list[TextContent]:
from skill_seekers.cli.adaptors import get_adaptor
skill_dir = Path(args.get("skill_dir"))
target = args.get("target", "claude")
target = args.get("target", "auto")
if target == "auto":
from skill_seekers.cli.agent_client import AgentClient
target = AgentClient.detect_default_target()
mode = args.get("mode", "local")
api_key = args.get("api_key")
@@ -348,8 +360,8 @@ async def enhance_skill_tool(args: dict) -> list[TextContent]:
output_lines.append("")
if mode == "local":
# Use local enhancement (Claude Code)
output_lines.append("Using Claude Code Max (local, no API key required)")
# Use local enhancement (AI coding agent)
output_lines.append("Using AI coding agent (local, no API key required)")
output_lines.append("Running enhancement in headless mode...")
output_lines.append("")
@@ -437,7 +449,7 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
- auto_upload (bool): Upload after packaging (default: True)
- unlimited (bool): Remove page limits (default: False)
- dry_run (bool): Preview only (default: False)
- target (str): Target LLM platform (default: "claude")
- target (str): Target LLM platform (default: "auto")
Returns:
List of TextContent with workflow progress and results
@@ -455,7 +467,14 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
auto_upload = args.get("auto_upload", True)
unlimited = args.get("unlimited", False)
dry_run = args.get("dry_run", False)
target = args.get("target", "claude")
target = args.get("target", "auto")
marketplace_arg = args.get("marketplace")
marketplace_category = args.get("marketplace_category", "development")
create_branch = args.get("create_branch", False)
if target == "auto":
from skill_seekers.cli.agent_client import AgentClient
target = AgentClient.detect_default_target()
# Get platform adaptor
try:
@@ -559,6 +578,7 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
with open(workflow_state["config_path"]) as f:
config = json.load(f)
workflow_state["skill_name"] = config.get("name", "unknown")
workflow_state["config_data"] = config
except Exception as e:
return [
TextContent(
@@ -637,7 +657,7 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
output_lines.append(stdout)
workflow_state["phases_completed"].append("enhance_skill")
else:
output_lines.append(" [DRY RUN] Would enhance SKILL.md with Claude Code")
output_lines.append(" [DRY RUN] Would enhance SKILL.md with AI agent")
output_lines.append("")
@@ -702,6 +722,7 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
output_lines.append("")
# ===== PHASE 5: Upload (Optional) =====
has_api_key = False # Initialize before conditional block
if auto_upload:
phase_num = "5/5" if config_name else "4/4"
output_lines.append(f"📤 PHASE {phase_num}: Upload to {adaptor.PLATFORM_NAME}")
@@ -767,6 +788,62 @@ async def install_skill_tool(args: dict) -> list[TextContent]:
output_lines.append("")
# ===== PHASE 6: Publish to Marketplace (Optional) =====
marketplace_targets = []
if marketplace_arg:
marketplace_targets.append(
{"marketplace": marketplace_arg, "category": marketplace_category}
)
else:
cd = workflow_state.get("config_data", {})
if isinstance(cd, dict):
marketplace_targets = cd.get("marketplace_targets", [])
if marketplace_targets:
phase_num = len(workflow_state["phases_completed"]) + 1
output_lines.append(f"{'=' * 70}")
output_lines.append(
f"PHASE {phase_num}: Publish to Marketplace"
f" ({len(marketplace_targets)} target{'s' if len(marketplace_targets) > 1 else ''})"
)
output_lines.append(f"{'=' * 70}")
output_lines.append("")
if not dry_run:
from .marketplace_tools import publish_to_marketplace_tool
for mp_target in marketplace_targets:
mp_name = mp_target.get("marketplace", "")
mp_cat = mp_target.get("category", "development")
output_lines.append(f"Publishing to marketplace '{mp_name}'...")
try:
pub_result = await publish_to_marketplace_tool(
{
"skill_dir": workflow_state["skill_dir"],
"marketplace": mp_name,
"category": mp_cat,
"create_branch": create_branch,
"force": True,
}
)
pub_output = pub_result[0].text if pub_result else "No output"
output_lines.append(pub_output)
workflow_state["phases_completed"].append(
f"publish_to_marketplace({mp_name})"
)
except Exception as e:
output_lines.append(f"Failed to publish to '{mp_name}': {str(e)}")
output_lines.append("")
else:
for mp_target in marketplace_targets:
mp_name = mp_target.get("marketplace", "")
mp_cat = mp_target.get("category", "development")
output_lines.append(
f" [DRY RUN] Would publish to marketplace '{mp_name}' (category: {mp_cat})"
)
output_lines.append("")
# ===== WORKFLOW SUMMARY =====
output_lines.append("=" * 70)
output_lines.append("✅ WORKFLOW COMPLETE")

View File

@@ -826,3 +826,63 @@ To see all sources:
except Exception as e:
return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
async def push_config_tool(args: dict) -> list[TextContent]:
"""
Push a config to a registered config source repository.
Validates the config, places it in the correct category directory,
commits, and pushes to the source repo.
Args:
args: Dictionary containing:
- config_path: Path to config JSON file (required)
- source_name: Registered source name, e.g., "spyke" (required)
- category: Category directory (e.g., "game-engines"). Auto-detected if omitted.
- create_branch: Create feature branch instead of pushing to main (default: false)
- force: Overwrite existing config (default: false)
Returns:
List of TextContent with push results
"""
config_path = args.get("config_path")
source_name = args.get("source_name")
category = args.get("category", "auto")
create_branch = args.get("create_branch", False)
force = args.get("force", False)
if not config_path:
return [TextContent(type="text", text="❌ Missing required parameter: config_path")]
if not source_name:
return [TextContent(type="text", text="❌ Missing required parameter: source_name")]
try:
from skill_seekers.mcp.config_publisher import ConfigPublisher
publisher = ConfigPublisher()
result = publisher.publish(
config_path=config_path,
source_name=source_name,
category=category,
create_branch=create_branch,
force=force,
)
output = f"""✅ Config pushed successfully!
📄 Config: {result["config_name"]}
📂 Path: {result["config_path"]}
🏷️ Category: {result["category"]}
📦 Source: {result["source"]}
🔀 Branch: {result["branch"]}
📝 Commit: {result["commit_sha"]}
💬 Message: {result["message"]}
To fetch this config:
fetch_config(source="{result["source"]}", config_name="{result["config_name"]}")
"""
return [TextContent(type="text", text=output)]
except Exception as e:
return [TextContent(type="text", text=f"❌ Push failed: {str(e)}")]

View File

@@ -0,0 +1,131 @@
name: ai-assistant-internals
description: Deep analysis of AI assistant CLI internals including tool registry, context pipeline, MCP integration, sub-agent coordination, and cost optimization
version: "1.0"
applies_to:
- codebase_analysis
- github_analysis
variables:
depth: comprehensive
stages:
- name: tool_registry_analysis
type: custom
target: tools
uses_history: false
enabled: true
prompt: >
Analyze the tool registry and dispatch system in this AI assistant codebase.
For each tool/agent capability:
1. Tool name and purpose
2. Input schema (parameters, types, required vs optional)
3. Output schema (return types, error conditions)
4. Dispatch mechanism (how the LLM selects and invokes it)
5. Related tools that are commonly chained together
Output JSON with:
- "tools": array of {name, purpose, input_schema, output_schema, dispatch_mechanism, related_tools[]}
- "tool_categories": grouping by function (file_ops, web_search, agent_mgmt, shell_exec, etc.)
- "dispatch_pattern": how tool calls are parsed, validated, and routed
- name: context_pipeline
type: custom
target: context
uses_history: true
enabled: true
prompt: >
Document how user queries, system context, conversation history, and file context are assembled into LLM prompts.
Cover:
1. System prompt construction (static vs dynamic parts)
2. User context collection (current directory, git state, open files)
3. Conversation history management (truncation, summarization, checkpointing)
4. File context injection (how code is read and formatted for the model)
5. Context window optimization strategies
Output JSON with:
- "system_prompt_structure": sections and their purposes
- "context_sources": array of {source, description, priority, fallback_behavior}
- "history_management": truncation/summary strategy
- "file_context_format": how files are represented in prompts
- "optimization_strategies": techniques to stay within token limits
- name: mcp_patterns
type: custom
target: mcp
uses_history: true
enabled: true
prompt: >
Extract Model Context Protocol (MCP) integration patterns from this codebase.
Document:
1. MCP client initialization and configuration
2. Server discovery and connection lifecycle
3. Tool/resource exposure via MCP
4. Authentication and security boundaries
5. Error handling for MCP server failures
Output JSON with:
- "mcp_client": initialization and config patterns
- "server_management": discovery, connect, disconnect, health checks
- "exposed_capabilities": tools/resources surfaced to MCP
- "security_model": auth, sandboxing, permission boundaries
- "failure_modes": retry, fallback, and user notification strategies
- name: agent_coordination
type: custom
target: agents
uses_history: true
enabled: true
prompt: >
Analyze sub-agent creation, task delegation, and multi-agent coordination patterns.
Identify:
1. Agent lifecycle (create, run, pause, resume, terminate)
2. Task delegation strategies (how work is split across agents)
3. Inter-agent communication (message passing, shared state)
4. Result aggregation and merging from multiple agents
5. Parent-child relationship management and oversight
Output JSON with:
- "agent_lifecycle": states and transitions
- "delegation_patterns": strategies for splitting work
- "communication_model": how agents share data
- "result_aggregation": merging outputs from parallel agents
- "oversight_mechanisms": how parent agents monitor children
- name: cost_optimization
type: custom
target: cost
uses_history: true
enabled: true
prompt: >
Review token usage tracking, cost estimation, and optimization strategies.
Document:
1. How input/output tokens are measured per request
2. Cost estimation methodology (pricing models, caching)
3. Optimization techniques (prompt compression, selective context, model tiering)
4. Budget controls and user-facing cost feedback
5. Trade-offs between cost and response quality
Output JSON with:
- "token_measurement": how tokens are counted and attributed
- "cost_estimation": pricing logic and display
- "optimization_techniques": array of {name, description, impact, implementation_location}
- "budget_controls": limits, warnings, and enforcement
- "quality_tradeoffs": when and how cost is prioritized over quality
post_process:
reorder_sections:
- overview
- tool_registry
- context_pipeline
- mcp_integration
- agent_coordination
- cost_optimization
- examples
- advanced_topics
add_metadata:
enhanced: true
workflow: ai-assistant-internals
deep_analysis: true

View File

@@ -0,0 +1,214 @@
name: unity-game-dev
description: Unity game development patterns, MonoBehaviour lifecycle, component architecture, memory management, and C# best practices for Unity projects
version: "1.0"
applies_to:
- codebase_analysis
- github_analysis
- documentation
variables:
depth: comprehensive
stages:
- name: monobehaviour_lifecycle
type: custom
target: lifecycle
uses_history: false
enabled: true
prompt: >
Document all MonoBehaviour lifecycle patterns used in this codebase/library.
Cover:
1. Initialization order (Awake vs Start vs OnEnable)
2. Update loops (Update vs FixedUpdate vs LateUpdate) and when to use each
3. Destruction and cleanup (OnDestroy, OnDisable, OnApplicationQuit)
4. Coroutine lifecycle and cancellation patterns
5. Script execution order dependencies
For each pattern found:
- When to use it vs alternatives
- Common mistakes and gotchas
- Code example showing correct usage
Output JSON with "lifecycle_patterns" array of:
{name, phase, use_case, gotchas[], code_example, execution_order}
- name: component_architecture
type: custom
target: components
uses_history: true
enabled: true
prompt: >
Document the component-based architecture patterns.
Cover:
1. Component composition patterns (prefer composition over inheritance)
2. GetComponent usage and caching strategies
3. RequireComponent and dependency management
4. Prefab architecture and nested prefab patterns
5. ScriptableObject as data containers and event channels
6. Inspector serialization ([SerializeField], [Header], [Tooltip], custom editors)
For each pattern:
- Setup steps in Unity Editor
- C# implementation
- When to use vs alternatives
Output JSON with:
- "component_patterns": array of architecture patterns
- "scriptable_objects": data-driven design patterns
- "serialization": Inspector setup patterns
- name: dependency_injection
type: custom
target: di_patterns
uses_history: true
enabled: true
prompt: >
Document dependency injection patterns for Unity.
Cover:
1. Zenject/Extenject bindings (BindInterfacesAndSelfTo, FromComponentInHierarchy, AsSingle, AsCached)
2. VContainer registration patterns (if used)
3. Installer organization (MonoInstaller, ScriptableObjectInstaller, ProjectInstaller)
4. Factory patterns with DI (PlaceholderFactory, IFactory)
5. Signal/event bus patterns via DI
6. Scene-scoped vs project-scoped containers
For each pattern:
- Binding configuration
- Injection usage ([Inject], constructor injection)
- Lifecycle management
- Testing with mock bindings
Output JSON with:
- "di_bindings": binding patterns and when to use each
- "installers": installer organization
- "factories": factory patterns
- "testing": how to mock dependencies
- name: async_patterns
type: custom
target: async
uses_history: true
enabled: true
prompt: >
Document async/concurrent programming patterns for Unity.
Cover:
1. Coroutines (IEnumerator, yield return, StartCoroutine/StopCoroutine)
2. UniTask patterns (async/await, UniTask vs Task, cancellation tokens)
3. Async asset loading (Addressables, Resources.LoadAsync)
4. Threading considerations (main thread requirement, UnitySynchronizationContext)
5. DOTween async integration (AwaitForCompletion, ToUniTask)
6. Progress reporting and loading screens
For each pattern:
- When to use it vs alternatives
- Cancellation and cleanup
- Error handling
- Performance implications
Output JSON with:
- "coroutine_patterns": coroutine usage and gotchas
- "unitask_patterns": modern async patterns
- "loading_patterns": async loading strategies
- "thread_safety": main thread considerations
- name: memory_and_performance
type: custom
target: unity_performance
uses_history: true
enabled: true
prompt: >
Document Unity-specific memory management and performance patterns.
Cover:
1. Object pooling (instantiate/destroy avoidance, pool managers)
2. GC pressure reduction (avoid allocations in Update, struct vs class, NativeArray)
3. Asset memory lifecycle (loading, reference counting, unloading, Resources.UnloadUnusedAssets)
4. Texture/mesh memory optimization (atlasing, LOD, compression)
5. Draw call batching (static/dynamic batching, SRP Batcher, GPU instancing)
6. Profiler-guided optimization (Unity Profiler, Memory Profiler, Frame Debugger)
For each optimization:
- Problem it solves
- Implementation approach
- Measurable impact
- Common mistakes
Output JSON with:
- "pooling": object pool patterns
- "gc_optimization": allocation reduction techniques
- "asset_memory": asset lifecycle management
- "rendering": draw call and GPU optimizations
- "profiling": how to measure and verify
- name: unity_events
type: custom
target: events
uses_history: true
enabled: true
prompt: >
Document event and messaging patterns for Unity.
Cover:
1. C# events and delegates (event Action, EventHandler)
2. UnityEvent and Inspector-wired callbacks
3. ScriptableObject event channels (SOA event pattern)
4. Signal bus patterns (Zenject Signals, custom implementations)
5. Observer pattern implementations
6. Message broker / pub-sub patterns
For each pattern:
- When to use it (tight coupling vs loose coupling trade-offs)
- Memory leak prevention (unsubscribe in OnDestroy/OnDisable)
- Thread safety considerations
- Code example with subscribe/unsubscribe
Output JSON with:
- "event_patterns": array of event/messaging approaches
- "best_practices": subscription lifecycle management
- "comparisons": when to use which pattern
- name: editor_integration
type: custom
target: editor
uses_history: true
enabled: true
prompt: >
Document Unity Editor integration patterns.
Cover:
1. Custom Inspector/Editor scripts ([CustomEditor], EditorGUILayout)
2. PropertyDrawer and DecoratorDrawer patterns
3. EditorWindow for tool development
4. Gizmos and Handles for scene visualization
5. Menu items and context menus ([MenuItem], [ContextMenu])
6. Build pipeline hooks (IPreprocessBuildWithReport, IPostprocessBuildWithReport)
For each pattern:
- Use case and setup
- Code example
- Editor vs runtime separation (#if UNITY_EDITOR)
Output JSON with:
- "custom_inspectors": inspector customization patterns
- "editor_tools": tool development patterns
- "build_pipeline": build automation patterns
post_process:
reorder_sections:
- monobehaviour_lifecycle
- component_architecture
- dependency_injection
- async_patterns
- memory_and_performance
- unity_events
- editor_integration
add_metadata:
enhanced: true
workflow: unity-game-dev
framework: unity
has_lifecycle_docs: true
has_di_patterns: true
has_performance_guide: true