diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000..33f1e0e6 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,158 @@ +name: Sync Fork with Upstream + +on: + schedule: + - cron: "0 */12 * * *" + workflow_dispatch: + inputs: + dry_run: + description: "Sync but do not push (dry-run mode)" + required: false + default: "false" + type: boolean + +permissions: + contents: write + +concurrency: + group: sync-upstream + cancel-in-progress: false + +jobs: + sync: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + UPSTREAM_REPO: Alishahryar1/free-claude-code + UPSTREAM_BRANCH: main + FORK_BRANCH: main + + steps: + - name: Checkout fork + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch upstream + run: | + git remote add upstream "https://github.com/${{ env.UPSTREAM_REPO }}.git" 2>/dev/null || true + git fetch upstream "${{ env.UPSTREAM_BRANCH }}" --no-tags + + - name: Detect upstream force-push + id: force_push + run: | + MERGE_BASE=$(git merge-base HEAD "upstream/${{ env.UPSTREAM_BRANCH }}" 2>/dev/null || echo "") + if [ -z "$MERGE_BASE" ]; then + echo "detected=true" >> "$GITHUB_OUTPUT" + echo "::error::No common ancestor with upstream — upstream may have been force-pushed or rewritten." + exit 1 + fi + # Check that the merge-base is reachable from upstream (i.e. upstream still contains our shared history) + if ! git merge-base --is-ancestor "$MERGE_BASE" "upstream/${{ env.UPSTREAM_BRANCH }}"; then + echo "detected=true" >> "$GITHUB_OUTPUT" + echo "::error::Merge-base is not an ancestor of upstream HEAD — upstream history was rewritten." + exit 1 + fi + echo "detected=false" >> "$GITHUB_OUTPUT" + echo "merge_base=$MERGE_BASE" >> "$GITHUB_OUTPUT" + + - name: Check for new upstream commits + id: check + run: | + BEHIND=$(git rev-list --count HEAD.."upstream/${{ env.UPSTREAM_BRANCH }}") + if [ "$BEHIND" -eq 0 ]; then + echo "status=up-to-date" >> "$GITHUB_OUTPUT" + echo "::notice::Already up to date with upstream." + else + echo "status=behind" >> "$GITHUB_OUTPUT" + echo "behind=$BEHIND" >> "$GITHUB_OUTPUT" + UPSTREAM_HEAD=$(git rev-parse "upstream/${{ env.UPSTREAM_BRANCH }}") + echo "upstream_head=${UPSTREAM_HEAD}" >> "$GITHUB_OUTPUT" + echo "::notice::$BEHIND commit(s) behind upstream." + fi + + - name: Merge upstream (upstream wins on conflicts) + if: steps.check.outputs.status == 'behind' + id: merge + run: | + FORK_HEAD_BEFORE=$(git rev-parse HEAD) + echo "fork_head_before=${FORK_HEAD_BEFORE}" >> "$GITHUB_OUTPUT" + + if git merge "upstream/${{ env.UPSTREAM_BRANCH }}" -X theirs --no-edit \ + -m "Merge upstream/${{ env.UPSTREAM_BRANCH }} into ${{ env.FORK_BRANCH }} [skip ci]"; then + echo "result=merged" >> "$GITHUB_OUTPUT" + else + git merge --abort 2>/dev/null || true + echo "result=failed" >> "$GITHUB_OUTPUT" + echo "::error::Merge failed with tree/structural conflicts. Manual intervention required." + exit 1 + fi + + - name: Reapply fork overrides + if: steps.check.outputs.status == 'behind' + run: | + # Keep fork-owned workflow files to avoid workflow-permission push rejections. + git restore --source=HEAD^1 -- .github/workflows + python3 scripts/sync_readme.py + cp -f fork/claude-free claude-free + chmod +x claude-free + if [ -n "$(git status --porcelain)" ]; then + git add .github/workflows README.md claude-free + git commit -m "chore: reapply fork overrides after upstream sync [skip ci]" + fi + + - name: Push changes + if: steps.check.outputs.status == 'behind' && inputs.dry_run != 'true' + run: | + if git push origin "${{ env.FORK_BRANCH }}"; then + echo "::notice::Push succeeded." + else + echo "::error::Push failed. This is usually permissions or non-fast-forward. Re-run after resolving branch state." + exit 1 + fi + + - name: Dry-run notice + if: steps.check.outputs.status == 'behind' && inputs.dry_run == 'true' + run: | + echo "::notice::Dry-run mode — skipped push. The following commits would be pushed:" + git log --oneline "origin/${{ env.FORK_BRANCH }}..HEAD" + + - name: Summary + if: always() + run: | + { + echo "## Upstream Sync" + echo "" + if [ "${{ steps.check.outputs.status }}" = "up-to-date" ]; then + echo "Already up to date with upstream." + elif [ "${{ steps.merge.outputs.result }}" = "failed" ] || [ "${{ steps.force_push.outputs.detected }}" = "true" ]; then + echo "Sync **failed**. Manual intervention required." + echo "" + if [ "${{ steps.force_push.outputs.detected }}" = "true" ]; then + echo "**Reason:** Upstream force-push detected." + else + echo "**Reason:** Merge conflicts that could not be auto-resolved." + fi + else + echo "Merged **${{ steps.check.outputs.behind }}** upstream commit(s) via \`${{ steps.merge.outputs.result }}\` strategy." + echo "" + echo "Fork overrides reapplied after merge." + if [ "${{ inputs.dry_run }}" = "true" ]; then + echo "" + echo "> **Dry run** — changes were not pushed." + fi + echo "" + REPO="${{ github.repository }}" + BEFORE="${{ steps.merge.outputs.fork_head_before }}" + AFTER="$(git rev-parse HEAD)" + if [ -n "$BEFORE" ] && [ -n "$AFTER" ] && [ "$BEFORE" != "$AFTER" ]; then + echo "[View diff](https://github.com/${REPO}/compare/${BEFORE}...${AFTER})" + fi + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a84754d7..1ed14ee3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,7 @@ on: branches: [main, master] pull_request: branches: [main, master] + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -41,7 +42,7 @@ jobs: - name: Lint check run: uv run ruff format --check - - name: Style check + - name: Format check run: uv run ruff check - name: Type check diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 316f2716..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,43 +0,0 @@ -# AGENTIC DIRECTIVE - -> This file is identical to CLAUDE.md. Keep them in sync. - -## CODING ENVIRONMENT -- Always use `uv run` to run files instead of the global `python` command. -- Read `.env.example` for environment variables. -- All CI checks must pass; failing checks block merge. -- Add tests for new changes (including edge cases), then run `uv run pytest`. -- Run checks in this order: `uv run ruff format`, `uv run ruff check`, `uv run ty check`, `uv run pytest`. -- Do not add `# type: ignore` or `# ty: ignore`; fix the underlying type issue. -- All 5 checks are enforced in `tests.yml` on push/merge. - -## IDENTITY & CONTEXT -- You are an expert Software Architect and Systems Engineer. -- Goal: Zero-defect, root-cause-oriented engineering for bugs; test-driven engineering for new features. Think carefully; no need to rush. -- Code: Write the simplest code possible. Keep the codebase minimal and modular. - -## ARCHITECTURE PRINCIPLES (see PLAN.md) -- **Shared utilities**: Extract common logic into shared packages (e.g. `providers/common/`). Do not have one provider import from another provider's utils. -- **DRY**: Extract shared base classes to eliminate duplication. Prefer composition over copy-paste. -- **Encapsulation**: Use accessor methods for internal state (e.g. `set_current_task()`), not direct `_attribute` assignment from outside. -- **Provider-specific config**: Keep provider-specific fields (e.g. `nim_settings`) in provider constructors, not in the base `ProviderConfig`. -- **Dead code**: Remove unused code, legacy systems, and hardcoded values. Use settings/config instead of literals (e.g. `settings.provider_type` not `"nvidia_nim"`). -- **Performance**: Use list accumulation for strings (not `+=` in loops), cache env vars at init, prefer iterative over recursive when stack depth matters. -- **Platform-agnostic naming**: Use generic names (e.g. `PLATFORM_EDIT`) not platform-specific ones (e.g. `TELEGRAM_EDIT`) in shared code. -- **No type ignores**: Do not add `# type: ignore` or `# ty: ignore`. Fix the underlying type issue. -- **Backward compatibility**: When moving modules, add re-exports from old locations so existing imports keep working. - -## COGNITIVE WORKFLOW -1. **ANALYZE**: Read relevant files. Do not guess. -2. **PLAN**: Map out the logic. Identify root cause or required changes. Order changes by dependency. -3. **EXECUTE**: Fix the cause, not the symptom. Execute incrementally with clear commits. -4. **VERIFY**: Run tests and linting. Confirm the fix via logs or output. -5. **SPECIFICITY**: Do exactly as much as asked; nothing more, nothing less. -6. **PROPAGATION**: Changes impact multiple files; propagate updates correctly. - -## SUMMARY STANDARDS -- Summaries must be technical and granular. -- Include: [Files Changed], [Logic Altered], [Verification Method], [Residual Risks]. - -## TOOLS -- Prefer built-in tools (grep, read_file, etc.) over manual workflows. Check tool availability before use. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 56f6c3f1..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,43 +0,0 @@ -# AGENTIC DIRECTIVE - -> This file is identical to AGENTS.md. Keep them in sync. - -## CODING ENVIRONMENT -- Always use `uv run` to run files instead of the global `python` command. -- Read `.env.example` for environment variables. -- All CI checks must pass; failing checks block merge. -- Add tests for new changes (including edge cases), then run `uv run pytest`. -- Run checks in this order: `uv run ruff format`, `uv run ruff check`, `uv run ty check`, `uv run pytest`. -- Do not add `# type: ignore` or `# ty: ignore`; fix the underlying type issue. -- All 5 checks are enforced in `tests.yml` on push/merge. - -## IDENTITY & CONTEXT -- You are an expert Software Architect and Systems Engineer. -- Goal: Zero-defect, root-cause-oriented engineering for bugs; test-driven engineering for new features. Think carefully; no need to rush. -- Code: Write the simplest code possible. Keep the codebase minimal and modular. - -## ARCHITECTURE PRINCIPLES (see PLAN.md) -- **Shared utilities**: Extract common logic into shared packages (e.g. `providers/common/`). Do not have one provider import from another provider's utils. -- **DRY**: Extract shared base classes to eliminate duplication. Prefer composition over copy-paste. -- **Encapsulation**: Use accessor methods for internal state (e.g. `set_current_task()`), not direct `_attribute` assignment from outside. -- **Provider-specific config**: Keep provider-specific fields (e.g. `nim_settings`) in provider constructors, not in the base `ProviderConfig`. -- **Dead code**: Remove unused code, legacy systems, and hardcoded values. Use settings/config instead of literals (e.g. `settings.provider_type` not `"nvidia_nim"`). -- **Performance**: Use list accumulation for strings (not `+=` in loops), cache env vars at init, prefer iterative over recursive when stack depth matters. -- **Platform-agnostic naming**: Use generic names (e.g. `PLATFORM_EDIT`) not platform-specific ones (e.g. `TELEGRAM_EDIT`) in shared code. -- **No type ignores**: Do not add `# type: ignore` or `# ty: ignore`. Fix the underlying type issue. -- **Backward compatibility**: When moving modules, add re-exports from old locations so existing imports keep working. - -## COGNITIVE WORKFLOW -1. **ANALYZE**: Read relevant files. Do not guess. -2. **PLAN**: Map out the logic. Identify root cause or required changes. Order changes by dependency. -3. **EXECUTE**: Fix the cause, not the symptom. Execute incrementally with clear commits. -4. **VERIFY**: Run tests and linting. Confirm the fix via logs or output. -5. **SPECIFICITY**: Do exactly as much as asked; nothing more, nothing less. -6. **PROPAGATION**: Changes impact multiple files; propagate updates correctly. - -## SUMMARY STANDARDS -- Summaries must be technical and granular. -- Include: [Files Changed], [Logic Altered], [Verification Method], [Residual Risks]. - -## TOOLS -- Prefer built-in tools (grep, read_file, etc.) over manual workflows. Check tool availability before use. \ No newline at end of file diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 963cb4b4..00000000 --- a/PLAN.md +++ /dev/null @@ -1,450 +0,0 @@ -# Architecture Improvement Plan - -## Overview - -This plan addresses 7 categories of improvements found during the code review: -provider code duplication, mislocated shared utilities, encapsulation leaks, -dead code, performance issues, directory structure, and minor fixes. - -Changes are ordered by dependency: foundational moves first, then refactors -that build on them, then independent cleanup. - -**CI requirements**: Every step must pass all 5 CI checks: -1. No `# type: ignore` / `# ty: ignore` -2. `uv run ruff format` -3. `uv run ruff check` -4. `uv run ty check` -5. `uv run pytest` - ---- - -## Phase 1: Extract shared provider utilities into `providers/common/` - -**Goal**: Eliminate the coupling where OpenRouter and LMStudio import from -`providers.nvidia_nim.utils` and `providers.nvidia_nim.errors`. - -### Step 1.1: Create `providers/common/` package - -Move these files from `providers/nvidia_nim/utils/` → `providers/common/`: -- `sse_builder.py` → `providers/common/sse_builder.py` -- `message_converter.py` → `providers/common/message_converter.py` -- `think_parser.py` → `providers/common/think_parser.py` -- `heuristic_tool_parser.py` → `providers/common/heuristic_tool_parser.py` - -Move from `providers/nvidia_nim/`: -- `errors.py` → `providers/common/error_mapping.py` - -Create `providers/common/__init__.py` with the same re-exports that -`providers/nvidia_nim/utils/__init__.py` currently has, plus `map_error`. - -### Step 1.2: Update `providers/nvidia_nim/utils/__init__.py` - -Change it to re-export from `providers.common` for backward compatibility: -```python -from providers.common import ( - SSEBuilder, ContentBlockManager, map_stop_reason, - ThinkTagParser, ContentType, ContentChunk, - HeuristicToolParser, - AnthropicToOpenAIConverter, get_block_attr, get_block_type, -) -``` - -Similarly update `providers/nvidia_nim/errors.py` to re-export: -```python -from providers.common.error_mapping import map_error -``` - -### Step 1.3: Update direct consumers to import from `providers.common` - -**Source files** (change imports): -- `providers/open_router/client.py` (lines 13-19) → import from `providers.common` -- `providers/lmstudio/client.py` (lines 13-19) → import from `providers.common` -- `providers/open_router/request.py` (line 5) → import from `providers.common.message_converter` -- `providers/lmstudio/request.py` (line 5) → import from `providers.common.message_converter` -- `providers/nvidia_nim/client.py` (lines 14-21, relative imports `from .errors` and `from .utils`) → import from `providers.common` -- `providers/nvidia_nim/request.py` (line 6, `from .utils.message_converter`) → import from `providers.common.message_converter` - -**Test files** (change imports): -- `tests/test_sse_builder.py` (line 7) -- `tests/test_lmstudio.py` (inline imports at ~8 locations) -- `tests/test_subagent_interception.py` (line 5) -- `tests/test_parsers.py` (lines 3-4) -- `tests/test_streaming_errors.py` (inline imports at ~8 locations) -- `tests/test_converter.py` (lines 3, 276) -- `tests/test_error_mapping.py` (line 9) - -### Step 1.4: Verify - -- `uv run pytest` — all tests pass -- `uv run ty check` — no type errors -- `uv run ruff check && uv run ruff format` - ---- - -## Phase 2: Extract shared streaming base class (`OpenAICompatibleProvider`) - -**Goal**: Eliminate ~400 lines of duplicated streaming logic across the 3 -provider clients. - -### Step 2.1: Create `providers/openai_compat.py` - -Create `OpenAICompatibleProvider(BaseProvider)` that contains the shared logic: - -```python -class OpenAICompatibleProvider(BaseProvider): - _client: AsyncOpenAI - _global_rate_limiter: GlobalRateLimiter - _provider_name: str # "NIM", "OPENROUTER", "LMSTUDIO" — used in log tags - - def __init__(self, config, *, provider_name, base_url, api_key, nim_settings=None): - # shared __init__: create AsyncOpenAI client, rate limiter - - def _build_request_body(self, request) -> dict: - raise NotImplementedError # each provider implements - - async def stream_response(self, request, input_tokens, *, request_id): - # shared: logger.contextualize + delegate to _stream_response_impl - - async def _stream_response_impl(self, request, input_tokens, request_id): - # THE shared ~180-line streaming loop, currently duplicated 3x - - def _handle_extra_reasoning(self, delta, sse) -> Iterator[str]: - """Hook for OpenRouter's reasoning_details. Default: no-op.""" - return iter(()) - - def _process_tool_call(self, tc, sse): - # shared ~40-line method - - def _flush_task_arg_buffers(self, sse): - # shared 3-line method -``` - -### Step 2.2: Refactor `NvidiaNimProvider` - -Reduce to: -```python -class NvidiaNimProvider(OpenAICompatibleProvider): - def __init__(self, config): - super().__init__(config, provider_name="NIM", - base_url=config.base_url or NVIDIA_NIM_BASE_URL, - api_key=config.api_key, - nim_settings=config.nim_settings) - - def _build_request_body(self, request): - return build_request_body(request, self._nim_settings) -``` - -### Step 2.3: Refactor `OpenRouterProvider` - -Reduce to: -```python -class OpenRouterProvider(OpenAICompatibleProvider): - def __init__(self, config): - super().__init__(config, provider_name="OPENROUTER", - base_url=config.base_url or OPENROUTER_BASE_URL, - api_key=config.api_key) - - def _build_request_body(self, request): - return build_request_body(request) - - def _handle_extra_reasoning(self, delta, sse): - # Handle reasoning_details for StepFun models (8 lines) - ... -``` - -### Step 2.4: Refactor `LMStudioProvider` - -Reduce to: -```python -class LMStudioProvider(OpenAICompatibleProvider): - def __init__(self, config): - super().__init__(config, provider_name="LMSTUDIO", - base_url=config.base_url or LMSTUDIO_DEFAULT_BASE_URL, - api_key=config.api_key or "lm-studio") - - def _build_request_body(self, request): - return build_request_body(request) -``` - -### Step 2.5: Verify - -- All existing tests must pass without modification (public interface unchanged) -- `uv run pytest && uv run ty check && uv run ruff format && uv run ruff check` - ---- - -## Phase 3: Fix encapsulation violations - -### Step 3.1: Add `MessageTree.set_current_task(task)` method - -In `messaging/tree_data.py`, add: -```python -def set_current_task(self, task: Optional[asyncio.Task]) -> None: - """Set the current processing task. Caller must hold lock.""" - self._current_task = task -``` - -Update `messaging/tree_processor.py` lines 117 and 155: -```python -# Before: tree._current_task = asyncio.create_task(...) -# After: tree.set_current_task(asyncio.create_task(...)) -``` - -### Step 3.2: Move `nim_settings` out of `ProviderConfig` base - -In `providers/base.py`, remove `nim_settings` from `ProviderConfig`. - -Add it as a field in `NvidiaNimProvider.__init__` or pass it directly -in the provider-specific config. The `OpenAICompatibleProvider` base class -stores it as `Optional[NimSettings]` only if passed. - -Update `api/dependencies.py` where `ProviderConfig` is constructed — only -pass `nim_settings` for the NIM provider. - -### Step 3.3: Verify - -- `uv run pytest && uv run ty check` - ---- - -## Phase 4: Remove dead code - -### Step 4.1: Remove legacy `SessionRecord` system - -In `messaging/session.py`: -- Remove `SessionRecord` dataclass (lines 18-28) -- Remove `self._sessions` dict and `self._msg_to_session` dict (lines 41-44) -- Remove `self._make_key()` method (lines 56-58) — note: keep `_make_chat_key()` which is still used -- Remove legacy session loading from `_load()` (lines 73-89) — keep tree - and message_log loading -- Remove `self._sessions` from `_save()` serialization (line 138) -- Remove `self._sessions.clear()` and `self._msg_to_session.clear()` from - `clear_all()` (lines 247-248) -- Remove the unused `import` for `dataclasses.asdict` if no longer needed - (currently used only to serialize `SessionRecord`) - -### Step 4.2: Fix hardcoded provider in root endpoint - -In `api/routes.py:102`: -```python -# Before: "provider": "nvidia_nim", -# After: "provider": settings.provider_type, -``` - -### Step 4.3: Verify - -- `uv run pytest && uv run ty check` - ---- - -## Phase 5: Performance improvements - -### Step 5.1: Use list-based string accumulation in transcript segments - -In `messaging/transcript.py`: - -**`ThinkingSegment`** — change from `self.text += t` to list accumulation: -```python -def __init__(self): - super().__init__(kind="thinking") - self._parts: list[str] = [] - -def append(self, t: str) -> None: - if t: - self._parts.append(t) - -@property -def text(self) -> str: - return "".join(self._parts) -``` - -Do the same for **`TextSegment`**. - -For **`ToolCallSegment.append_input_delta`** — same pattern. Also update -`set_initial_input()` to do `self._parts = [inp]` instead of -`self.input_text = inp`. - -Update `render()` methods and any test that accesses `.text` or -`.input_text` directly to use the property. - -### Step 5.2: Cache `MAX_MESSAGE_LOG_ENTRIES_PER_CHAT` at init time - -In `messaging/session.py`, `SessionStore.__init__`: -```python -cap_raw = os.getenv("MAX_MESSAGE_LOG_ENTRIES_PER_CHAT", "").strip() -self._message_log_cap: int | None = int(cap_raw) if cap_raw else None -``` - -Replace the per-call `os.getenv()` in `record_message_id()` (lines 215-229) -with `self._message_log_cap`. - -### Step 5.3: Use iterative DFS in `MessageTree.get_descendants` - -In `messaging/tree_data.py`, replace the recursive implementation: -```python -def get_descendants(self, node_id: str) -> list[str]: - if node_id not in self._nodes: - return [] - result = [] - stack = [node_id] - while stack: - nid = stack.pop() - result.append(nid) - node = self._nodes.get(nid) - if node: - stack.extend(node.children_ids) - return result -``` - -### Step 5.4: Verify - -- `uv run pytest` — all tests pass (especially transcript and tree tests) -- `uv run ty check` - ---- - -## Phase 6: Minor fixes and cleanup - -### Step 6.1: Remove `if False: yield ""` hack in `BaseProvider` - -In `providers/base.py`, replace the abstract method body: -```python -@abstractmethod -async def stream_response(self, ...) -> AsyncIterator[str]: - """Stream response in Anthropic SSE format.""" - ... -``` - -Note: This requires verifying that ty/mypy accepts `...` as a valid body -for an abstract async generator. If not, keep a minimal workaround but -add a comment explaining why. - -### Step 6.2: Clean up `messaging/handler.py` log message naming - -Lines 482, 491, 495, 497 and 507 say `TELEGRAM_EDIT` but the handler is -platform-agnostic. Rename to `PLATFORM_EDIT`: -```python -# line 482: TELEGRAM_EDIT → PLATFORM_EDIT -# line 491: TELEGRAM_EDIT_TEXT → PLATFORM_EDIT_TEXT -# line 495: TELEGRAM_EDIT_PREVIEW_HEAD → PLATFORM_EDIT_PREVIEW_HEAD -# line 497: TELEGRAM_EDIT_PREVIEW_TAIL → PLATFORM_EDIT_PREVIEW_TAIL -# line 507: Failed to update Telegram → Failed to update platform -``` - -### Step 6.3: Verify - -- `uv run ruff format && uv run ruff check && uv run ty check && uv run pytest` - ---- - -## Phase 7: Directory restructuring (messaging/ and tests/) - -**Note**: This phase has the highest risk of merge conflicts. It should be -done last and in one commit to minimize churn. - -### Step 7.1: Create `messaging/platforms/` sub-package - -Move: -- `messaging/base.py` → `messaging/platforms/base.py` -- `messaging/discord.py` → `messaging/platforms/discord.py` -- `messaging/telegram.py` → `messaging/platforms/telegram.py` -- `messaging/factory.py` → `messaging/platforms/factory.py` - -Create `messaging/platforms/__init__.py` re-exporting key symbols. -Update `messaging/__init__.py` to import from `messaging.platforms`. - -### Step 7.2: Create `messaging/rendering/` sub-package - -Move: -- `messaging/discord_markdown.py` → `messaging/rendering/discord_markdown.py` -- `messaging/telegram_markdown.py` → `messaging/rendering/telegram_markdown.py` - -Create `messaging/rendering/__init__.py`. -Update `messaging/handler.py` imports. - -### Step 7.3: Create `messaging/trees/` sub-package - -Move: -- `messaging/tree_data.py` → `messaging/trees/data.py` -- `messaging/tree_repository.py` → `messaging/trees/repository.py` -- `messaging/tree_processor.py` → `messaging/trees/processor.py` -- `messaging/tree_queue.py` → `messaging/trees/queue_manager.py` - -Create `messaging/trees/__init__.py` re-exporting `TreeQueueManager`, -`MessageTree`, `MessageNode`, `MessageState`. - -Update `messaging/__init__.py` re-exports. - -### Step 7.4: Organize `tests/` to mirror source - -Create subdirectories: -``` -tests/ - api/ ← test_api.py, test_routes_optimizations.py, test_app_lifespan_and_errors.py, etc. - providers/ ← test_nvidia_nim.py, test_open_router.py, test_lmstudio.py, etc. - messaging/ ← test_handler.py, test_tree_*.py, test_telegram.py, test_discord_*.py, etc. - cli/ ← test_cli.py, test_cli_manager_edge_cases.py, test_process_registry.py - config/ ← test_config.py, test_logging_config.py -``` - -Update `conftest.py` path if needed. Ensure pytest discovers all tests. - -### Step 7.5: Maintain backward-compatible re-exports - -Every moved module must have re-exports from the old location (via the -package `__init__.py`) so that any external consumer or existing import -path continues to work. These re-exports can be removed in a future -breaking version. - -### Step 7.6: Verify - -- `uv run pytest` — all 56+ test files discovered and passing -- `uv run ty check` — no broken imports -- `uv run ruff check && uv run ruff format` - ---- - -## Execution Order & Dependencies - -``` -Phase 1 (shared utils extraction) - └→ Phase 2 (shared base class) — depends on Phase 1 - └→ Phase 3 (encapsulation) — depends on Phase 2 for nim_settings -Phase 4 (dead code) — independent -Phase 5 (performance) — independent -Phase 6 (minor fixes) — independent -Phase 7 (directory restructure) — should be done LAST -``` - -Phases 4, 5, 6 are independent of each other and of Phase 2. They can be -done in any order or in parallel. - -Phase 7 must come after all other phases to avoid rebasing moved files. - ---- - -## Risk Assessment - -| Phase | Risk | Mitigation | -|-------|------|-----------| -| 1 | Import breakage in tests | Backward-compat re-exports in old location | -| 2 | Behavioral change in streaming | Tests cover all 3 providers; run full suite | -| 3 | `nim_settings` removal from base config | Check all `ProviderConfig` construction sites | -| 4 | Legacy session data stops loading | Only remove write path; keep read if needed | -| 5 | String accumulation changes rendering | Transcript tests exercise rendering thoroughly | -| 7 | Massive import churn, merge conflicts | Do in single commit, last phase | - ---- - -## Estimated Scope - -| Phase | Files Changed | Lines Changed (approx) | -|-------|--------------|----------------------| -| 1 | ~20 | +80 / -20 (new __init__ + import updates) | -| 2 | ~5 | +200 / -450 (net reduction ~250 lines) | -| 3 | ~4 | +15 / -10 | -| 4 | ~2 | +5 / -60 | -| 5 | ~3 | +30 / -15 | -| 6 | ~2 | +5 / -5 | -| 7 | ~60+ | +100 / -50 (mostly import changes) | -| **Total** | | **Net reduction ~200-300 lines** | diff --git a/README.md b/README.md index 27bc732a..c3856f88 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ # Free Claude Code +> **Enhanced Fork** of [Alishahryar1/free-claude-code](https://github.com/Alishahryar1/free-claude-code) +> This fork adds **simplified automated setup**, **enhanced Windows support**, and **improved installation guidelines** — open for all! + ### Use Claude Code CLI & VSCode — for free. No Anthropic API key required. [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT) @@ -12,10 +15,10 @@ [![Code style: Ruff](https://img.shields.io/badge/code%20formatting-ruff-f5a623.svg?style=for-the-badge)](https://github.com/astral-sh/ruff) [![Logging: Loguru](https://img.shields.io/badge/logging-loguru-4ecdc4.svg?style=for-the-badge)](https://github.com/Delgan/loguru) -A lightweight proxy server that translates Claude Code's Anthropic API calls into **NVIDIA NIM**, **OpenRouter**, or **LM Studio** format. +A lightweight proxy server that translates Claude Code's Anthropic API calls into **NVIDIA NIM**, **OpenRouter**, or **LM Studio** format. Get **40 free requests/min** on NVIDIA NIM, access **hundreds of models** on OpenRouter, or run **fully local** with LM Studio. -[Features](#features) · [Quick Start](#quick-start) · [How It Works](#how-it-works) · [Discord Bot](#discord-bot) · [Configuration](#configuration) +[Features](#features) · [Quick Start](#quick-start) · [How It Works](#how-it-works) · [Discord Bot](#discord-bot) · [Configuration](#configuration) · [Contributing](#contributing) --- @@ -26,7 +29,24 @@ Get **40 free requests/min** on NVIDIA NIM, access **hundreds of models** on Ope

Claude Code running via NVIDIA NIM — completely free

-## Features +## 🎯 What's New in This Fork + +This enhanced fork builds upon the original project with significant improvements: + +| Enhancement | Description | +|-------------|-------------| +| **🚀 Automated Setup Wizard** | One-click Windows installer with interactive PowerShell wizard | +| **📦 Prerequisite Auto-Install** | Automatically installs Python, Node.js, uv, PM2, and fzf | +| **🔧 Smart Configuration** | Interactive configuration wizard with validation | +| **🎨 Enhanced UI** | Retro-styled terminal UI with progress tracking | +| **🔄 Rollback Support** | Automatic backup and rollback on installation failure | +| **🖥️ Desktop Shortcuts** | Auto-generated shortcuts for easy access | +| **📝 Comprehensive Docs** | Detailed setup guide and troubleshooting documentation | +| **✅ Validation System** | Post-install validation ensures everything works | +| **🛠️ Repair Mode** | Built-in repair functionality for fixing issues | +| **🗑️ Clean Uninstall** | Complete uninstallation script with cleanup | + +## ✨ Features | Feature | Description | |---------|-------------| @@ -41,80 +61,123 @@ Get **40 free requests/min** on NVIDIA NIM, access **hundreds of models** on Ope | **Subagent Control** | Task tool interception forces `run_in_background=False` — no runaway subagents | | **Extensible** | Clean `BaseProvider` and `MessagingPlatform` ABCs — add new providers or platforms easily | -## Quick Start +## 🚀 Quick Start + +### 🪟 Automated Setup (Windows - Recommended) + +**The easiest way to get started on Windows:** + +1. **Download the repository** + ```bash + git clone https://github.com/rishiskhare/free-claude-code.git + cd free-claude-code + ``` + +2. **Run the one-click installer** + - **Double-click [`SETUP.bat`](SETUP.bat)** ← That's it! + - Or run in PowerShell: `.\setup\Setup-Wizard.ps1` + +3. **Follow the interactive wizard** + - The wizard will automatically install all prerequisites (Python, Node.js, uv, PM2, fzf) + - Select your AI provider (NVIDIA NIM, OpenRouter, or LM Studio) + - Enter your API key (get free NVIDIA key at [build.nvidia.com/settings/api-keys](https://build.nvidia.com/settings/api-keys)) + - Choose your preferred model + - Configure optional features (Discord/Telegram bots, voice transcription) + +4. **Done!** The server starts automatically and runs in the background. + +**What the wizard does:** +- ✅ Installs all prerequisites automatically +- ✅ Creates Python virtual environment +- ✅ Generates [`.env`](.env.example) configuration +- ✅ Sets up PM2 background service +- ✅ Creates desktop shortcuts +- ✅ Validates installation + +**Troubleshooting?** See [SETUP_GUIDE.md](SETUP_GUIDE.md) for detailed help. -### Prerequisites +--- + +### 📋 Manual Setup (All Platforms) + +If you prefer manual installation or are on Mac/Linux: + +#### Step 1: Install the prerequisites + +You need these before starting: + +| What | Where to get it | +| --- | --- | +| NVIDIA API key (free) | [build.nvidia.com/settings/api-keys](https://build.nvidia.com/settings/api-keys) | +| Claude Code CLI | [github.com/anthropics/claude-code](https://github.com/anthropics/claude-code) | +| uv (Python package runner) | [github.com/astral-sh/uv](https://github.com/astral-sh/uv) | +| PM2 (keeps the proxy running) | `npm install -g pm2` | +| fzf (fuzzy model picker) | [github.com/junegunn/fzf](https://github.com/junegunn/fzf) | -1. Get an API key (or use LM Studio locally): - - **NVIDIA NIM**: [build.nvidia.com/settings/api-keys](https://build.nvidia.com/settings/api-keys) - - **OpenRouter**: [openrouter.ai/keys](https://openrouter.ai/keys) - - **LM Studio**: No API key needed — run locally with [LM Studio](https://lmstudio.ai) -2. Install [Claude Code](https://github.com/anthropics/claude-code) -3. Install [uv](https://github.com/astral-sh/uv) -### Clone & Configure +#### Step 2: Clone the repo and add your API key ```bash -git clone https://github.com/Alishahryar1/free-claude-code.git +git clone https://github.com/rishiskhare/free-claude-code.git cd free-claude-code cp .env.example .env ``` -Choose your provider and edit `.env`: - -
-NVIDIA NIM (recommended — 40 req/min free) +Now open [`.env`](.env.example) and set the `NVIDIA_NIM_API_KEY` value: ```dotenv -PROVIDER_TYPE=nvidia_nim -NVIDIA_NIM_API_KEY=nvapi-your-key-here -MODEL=stepfun-ai/step-3.5-flash +NVIDIA_NIM_API_KEY="nvapi-paste-your-key-here" ``` -
+You only need to change that one key to get started. -
-OpenRouter (hundreds of models) +> **Want to use a different provider?** See [Providers](#providers) for OpenRouter (hundreds of models) or LM Studio (fully local). -```dotenv -PROVIDER_TYPE=open_router -OPENROUTER_API_KEY=sk-or-your-key-here -MODEL=stepfun/step-3.5-flash:free +#### Step 3: Start the proxy server + +```bash +pm2 start "uv run uvicorn server:app --host 0.0.0.0 --port 8082" --name "claude-proxy" ``` -
+That's it - the proxy is now running in the background. You can close this terminal and it keeps going. Use these commands to manage it: -
-LM Studio (fully local, no API key) +| Command | What it does | +| --- | --- | +| `pm2 logs claude-proxy` | See server logs (useful for troubleshooting) | +| `pm2 stop claude-proxy` | Stop the proxy | +| `pm2 restart claude-proxy` | Restart it (e.g., after editing `.env`) | +| `pm2 list` | Check if the proxy is running | -```dotenv -PROVIDER_TYPE=lmstudio -MODEL=lmstudio-community/qwen2.5-7b-instruct -``` +#### Step 4: Launch Claude Code -
+#### Option A: Terminal (CLI) -### Run It +Add this alias to your `~/.zshrc` (macOS) or `~/.bashrc` (Linux): -**Terminal 1** — Start the proxy server: +```bash +alias claude-free='/full/path/to/free-claude-code/claude-free' +``` + +Replace the path with where you cloned the repo (e.g., `/Users/yourname/Downloads/free-claude-code/`), then reload your shell: ```bash -uv run uvicorn server:app --host 0.0.0.0 --port 8082 +source ~/.zshrc # or: source ~/.bashrc ``` -**Terminal 2** — Run Claude Code: +Now you can run it from any directory: ```bash -ANTHROPIC_AUTH_TOKEN=freecc ANTHROPIC_BASE_URL=http://localhost:8082 claude +claude-free ``` -That's it! Claude Code now uses your configured provider for free. +You'll see a searchable list of every available model. Pick one and go. Just type a few letters to filter (e.g., type "kimi" to find Kimi K2.5 instantly). -
-VSCode Extension Setup +#### Option B: VSCode Extension -1. Start the proxy server (same as above). -2. Open Settings (`Ctrl + ,`) and search for `claude-code.environmentVariables`. +If you use the [Claude Code VSCode extension](https://marketplace.visualstudio.com/items?itemName=anthropics.claude-code), you can point it at the proxy too: + +1. Open VSCode Settings (`Cmd + ,` on macOS, `Ctrl + ,` on Linux/Windows). +2. Search for `claude-code.environmentVariables`. 3. Click **Edit in settings.json** and add: ```json @@ -124,338 +187,421 @@ That's it! Claude Code now uses your configured provider for free. ] ``` -4. Reload extensions. -5. **If you see the login screen** ("How do you want to log in?"): Click **Anthropic Console**, then authorize. The extension will start working. You may be redirected to buy credits in the browser — ignore that; the extension already works. +4. Reload the extension (or restart VSCode). +5. **If you see the login screen** ("How do you want to log in?"): Click **Anthropic Console**, then authorize. The extension will start working. You may be redirected to buy credits in the browser - ignore that; the extension already works. -To switch back to Anthropic models, comment out the added block and reload extensions. +That's it - the Claude Code panel in VSCode now uses NVIDIA NIM for free. To switch back to Anthropic, remove or comment out the block above and reload. -
+> **Tip:** To use a specific model from VSCode, set the token to `freecc:model-id` (e.g., `"freecc:moonshotai/kimi-k2.5"`). Otherwise it uses the `MODEL` value from your [`.env`](.env.example). ---- +## 🎨 Model-Specific Aliases (Optional) -## How It Works +You can also create aliases that skip the picker and go straight into a specific model. Add this to your `~/.zshrc` or `~/.bashrc`: -``` -┌─────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ -│ Claude Code │───────>│ Free Claude Code │───────>│ LLM Provider │ -│ CLI / VSCode │<───────│ Proxy (:8082) │<───────│ NIM / OR / LMS │ -└─────────────────┘ └──────────────────────┘ └──────────────────┘ - Anthropic API │ OpenAI-compatible - format (SSE) ┌───────┴────────┐ format (SSE) - │ Optimizations │ - ├────────────────┤ - │ Quota probes │ - │ Title gen skip │ - │ Prefix detect │ - │ Suggestion skip│ - │ Filepath mock │ - └────────────────┘ +```bash +alias claude-kimi='ANTHROPIC_BASE_URL="http://localhost:8082" ANTHROPIC_AUTH_TOKEN="freecc:moonshotai/kimi-k2.5" claude' ``` -- **Transparent proxy** — Claude Code sends standard Anthropic API requests to the proxy server -- **Request optimization** — 5 categories of trivial requests (quota probes, title generation, prefix detection, suggestions, filepath extraction) are intercepted and responded to instantly without using API quota -- **Format translation** — Real requests are translated from Anthropic format to the provider's OpenAI-compatible format and streamed back -- **Thinking tokens** — `` tags and `reasoning_content` fields are converted into native Claude thinking blocks so Claude Code renders them correctly +Swap out the model ID after `freecc:` to use any model. Then run `source ~/.zshrc` (or `source ~/.bashrc`). --- -## Providers +## 🔧 Configuration -| Provider | Cost | Rate Limit | Models | Best For | -|----------|------|------------|--------|----------| -| **NVIDIA NIM** | Free | 40 req/min | Kimi K2, GLM5, Devstral, MiniMax | Daily driver — generous free tier | -| **OpenRouter** | Free / Paid | Varies | 200+ (GPT-4o, Claude, Step, etc.) | Model variety, fallback options | -| **LM Studio** | Free (local) | Unlimited | Any GGUF model | Privacy, offline use, no rate limits | +### Providers -Switch providers by changing `PROVIDER_TYPE` in `.env`: +#### NVIDIA NIM (Free, Recommended) -| Provider | `PROVIDER_TYPE` | API Key Variable | Base URL | -|----------|-----------------|------------------|----------| -| NVIDIA NIM | `nvidia_nim` | `NVIDIA_NIM_API_KEY` | `integrate.api.nvidia.com/v1` | -| OpenRouter | `open_router` | `OPENROUTER_API_KEY` | `openrouter.ai/api/v1` | -| LM Studio | `lmstudio` | (none) | `localhost:1234/v1` | +Get 40 free requests per minute with NVIDIA NIM: -OpenRouter gives access to hundreds of models (StepFun, OpenAI, Anthropic, etc.) through a single API. Set `MODEL` to any OpenRouter model ID. +1. Get your free API key: [build.nvidia.com/settings/api-keys](https://build.nvidia.com/settings/api-keys) +2. Add to [`.env`](.env.example): + ```env + PROVIDER_TYPE=nvidia_nim + NVIDIA_NIM_API_KEY=nvapi-your-key-here + MODEL=stepfun-ai/step-3.5-flash + ``` -LM Studio runs locally — start the server in LM Studio's Developer tab or via `lms server start`, load a model, and set `MODEL` to the model identifier. +**Popular Models:** +- `stepfun-ai/step-3.5-flash` - Fast, balanced performance +- `deepseek-ai/deepseek-v3.1` - High quality reasoning +- `meta/llama-3.3-70b-instruct` - Strong reasoning capabilities +- `moonshotai/kimi-k2.5` - Long context (200K+ tokens) ---- +#### OpenRouter (Paid) -## Discord Bot +Access hundreds of models through OpenRouter: -Control Claude Code remotely from Discord. Send tasks, watch live progress, and manage multiple concurrent sessions. Discord is the default messaging platform; Telegram is also supported. +1. Get API key: [openrouter.ai/keys](https://openrouter.ai/keys) +2. Configure in [`.env`](.env.example): + ```env + PROVIDER_TYPE=open_router + OPENROUTER_API_KEY=sk-or-your-key-here + MODEL=anthropic/claude-3.5-sonnet + ``` -**Capabilities:** -- Tree-based message threading — reply to messages to fork conversations -- Session persistence across server restarts -- Live streaming of thinking tokens, tool calls, and results -- Up to 10 concurrent Claude CLI sessions -- **Voice notes** — send voice messages; they are transcribed to text and processed like regular prompts (see [Voice Notes](#voice-notes)) -- Commands: `/stop` (cancel tasks; reply to a message to stop only that task), `/clear` (standalone: reset all sessions; reply to a message to clear that branch downwards), `/stats` +#### LM Studio (Local) -### Setup +Run completely offline with local models: -1. **Create a Discord Bot** — Go to [Discord Developer Portal](https://discord.com/developers/applications), create an application, add a bot, and copy the token. Enable **Message Content Intent** under Bot settings. +1. Install LM Studio: [lmstudio.ai](https://lmstudio.ai/) +2. Load a model and start the local server +3. Configure in [`.env`](.env.example): + ```env + PROVIDER_TYPE=lmstudio + LM_STUDIO_BASE_URL=http://localhost:1234/v1 + MODEL=local-model + ``` -2. **Edit `.env`:** +### Optional Features -```dotenv +#### Discord Bot + +Enable remote autonomous coding via Discord: + +```env MESSAGING_PLATFORM=discord -DISCORD_BOT_TOKEN=your_discord_bot_token -ALLOWED_DISCORD_CHANNELS=123456789,987654321 +DISCORD_BOT_TOKEN=your-bot-token +ALLOWED_DISCORD_CHANNELS=channel-id-1,channel-id-2 ``` -> Enable Developer Mode in Discord (Settings → Advanced), then right-click a channel and "Copy ID" to get channel IDs. Comma-separate multiple channels. If empty, no channels are allowed. +#### Telegram Bot -3. **Configure the workspace** (where Claude will operate): +Enable remote autonomous coding via Telegram: -```dotenv -CLAUDE_WORKSPACE=./agent_workspace -ALLOWED_DIR=C:/Users/yourname/projects +```env +MESSAGING_PLATFORM=telegram +TELEGRAM_BOT_TOKEN=your-bot-token +ALLOWED_TELEGRAM_USER_ID=your-user-id ``` -4. **Start the server:** +#### Voice Transcription -```bash -uv run uvicorn server:app --host 0.0.0.0 --port 8082 +Enable voice note transcription with Whisper: + +```env +VOICE_NOTE_ENABLED=true +WHISPER_MODEL=base +WHISPER_DEVICE=cpu +HF_TOKEN=your-huggingface-token # Optional ``` -5. **Invite the bot** to your server (OAuth2 → URL Generator, scopes: `bot`, permissions: Read Messages, Send Messages, Manage Messages, Read Message History). Send a message in an allowed channel with a task. Claude responds with thinking tokens, tool calls as they execute, and the final result. Reply to messages to cancel tasks or clear branches (see Commands above). +Install voice dependencies: +```bash +uv sync --extra voice +``` -### Telegram (Alternative) +--- -To use Telegram instead, set `MESSAGING_PLATFORM=telegram` and configure: +## 🤖 How It Works -```dotenv -TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrSTUvwxYZ -ALLOWED_TELEGRAM_USER_ID=your_telegram_user_id +``` +┌─────────────────┐ +│ Claude Code │ (CLI or VSCode Extension) +│ CLI / VSCode │ +└────────┬────────┘ + │ Anthropic API format + │ (with ANTHROPIC_BASE_URL override) + ▼ +┌─────────────────────────────────────────┐ +│ Free Claude Code Proxy (this project) │ +│ ┌─────────────────────────────────┐ │ +│ │ FastAPI Server (port 8082) │ │ +│ │ • Request optimization │ │ +│ │ • Tool call parsing │ │ +│ │ • Thinking token support │ │ +│ │ • Rate limiting │ │ +│ └────────────┬────────────────────┘ │ +└───────────────┼─────────────────────────┘ + │ Provider-specific format + ▼ + ┌───────────┴───────────┐ + │ │ +┌───▼────┐ ┌──────▼─────┐ ┌────▼─────┐ +│ NVIDIA │ │ OpenRouter │ │LM Studio │ +│ NIM │ │ │ │ (Local) │ +└────────┘ └────────────┘ └──────────┘ ``` -Get a token from [@BotFather](https://t.me/BotFather); find your user ID via [@userinfobot](https://t.me/userinfobot). +The proxy intercepts Claude Code's API calls and: +1. **Optimizes requests** - Skips trivial calls (network probes, title generation, etc.) +2. **Translates format** - Converts Anthropic API format to provider-specific format +3. **Parses responses** - Extracts thinking tokens and tool calls +4. **Manages rate limits** - Implements smart throttling and backoff +5. **Returns results** - Sends back responses in Anthropic format -### Voice Notes +--- -Send voice messages on Telegram or Discord; they are transcribed to text and processed as regular prompts. Uses [faster-whisper](https://github.com/SYSTRAN/faster-whisper) — free, no API key, works offline. +## 🤝 Discord Bot -Install the optional voice extra: +Run Claude Code remotely through Discord with full autonomous capabilities: -```bash -uv sync --extra voice -``` - -**Configuration:** +### Features -| Variable | Description | Default | -|----------|-------------|---------| -| `VOICE_NOTE_ENABLED` | Enable voice note handling | `true` | -| `WHISPER_MODEL` | Model size: `tiny`, `base`, `small`, `medium`, `large-v2` | `base` | -| `WHISPER_DEVICE` | `cpu` \| `cuda` \| `auto` (auto = try GPU, fall back to CPU) | `cpu` | -| `HF_TOKEN` | Hugging Face token for faster model downloads (optional; [create one](https://huggingface.co/settings/tokens)) | — | +- **Tree-based Threading** - Conversations organized in threads +- **Session Persistence** - Resume conversations after restarts +- **Live Progress Updates** - Real-time status updates +- **Voice Note Support** - Send voice messages (transcribed with Whisper) +- **Multi-user Support** - Restrict access to specific channels +- **Transcript Export** - Download conversation history ---- +### Setup -## Models +1. Create a Discord bot at [discord.com/developers/applications](https://discord.com/developers/applications) +2. Enable these intents: Message Content, Guild Messages +3. Add bot to your server +4. Configure in [`.env`](.env.example): + ```env + MESSAGING_PLATFORM=discord + DISCORD_BOT_TOKEN=your-bot-token + ALLOWED_DISCORD_CHANNELS=channel-id-1,channel-id-2 + ``` +5. Restart the server: `pm2 restart claude-proxy` -
-NVIDIA NIM +### Usage -Full list in [`nvidia_nim_models.json`](nvidia_nim_models.json). +- **Start conversation**: Mention the bot in any allowed channel +- **Continue**: Reply to bot messages to continue the conversation +- **Voice notes**: Send voice messages (requires voice transcription enabled) +- **Export**: Use `/transcript` command to download conversation history -Popular models: -- `qwen/qwen3.5-397b-a17b` -- `z-ai/glm5` -- `stepfun-ai/step-3.5-flash` -- `moonshotai/kimi-k2.5` -- `minimaxai/minimax-m2.1` +--- -Browse: [build.nvidia.com](https://build.nvidia.com/explore/discover) +## 📁 Project Structure -Update model list: -```bash -curl "https://integrate.api.nvidia.com/v1/models" > nvidia_nim_models.json ``` +free-claude-code/ +├── api/ # FastAPI application +│ ├── app.py # Main application factory +│ ├── routes.py # API endpoints +│ ├── dependencies.py # Dependency injection +│ ├── detection.py # Request type detection +│ ├── optimization_handlers.py # Request optimizations +│ └── models/ # Pydantic models +├── providers/ # Provider implementations +│ ├── base.py # Base provider interface +│ ├── nvidia_nim/ # NVIDIA NIM provider +│ ├── open_router/ # OpenRouter provider +│ ├── lmstudio/ # LM Studio provider +│ └── common/ # Shared utilities +├── messaging/ # Messaging platform integrations +│ ├── platforms/ # Discord, Telegram implementations +│ ├── handler.py # Message handling logic +│ ├── trees/ # Conversation tree management +│ └── rendering/ # Markdown rendering +├── cli/ # CLI session management +│ ├── manager.py # Session manager +│ └── process_registry.py # Process tracking +├── config/ # Configuration +│ ├── settings.py # Settings management +│ └── logging_config.py # Logging setup +├── setup/ # Windows setup wizard +│ ├── Setup-Wizard.ps1 # Main wizard script +│ └── modules/ # PowerShell modules +├── tests/ # Comprehensive test suite +├── server.py # Entry point +├── .env.example # Configuration template +├── ecosystem.config.js # PM2 configuration +├── pyproject.toml # Python dependencies +└── README.md # This file +``` + +--- + +## 🧪 Development -
+### Running Tests -
-OpenRouter +```bash +# Run all tests +uv run pytest -Hundreds of models from StepFun, OpenAI, Anthropic, Google, and more. +# Run with coverage +uv run pytest --cov=. --cov-report=html -Popular models: -- `stepfun/step-3.5-flash:free` -- `deepseek/deepseek-r1-0528:free` -- `openai/gpt-oss-120b:free` +# Run specific test file +uv run pytest tests/api/test_routes.py -Browse: [openrouter.ai/models](https://openrouter.ai/models) +# Run with verbose output +uv run pytest -v +``` -Browse free models: [https://openrouter.ai/collections/free-models](https://openrouter.ai/collections/free-models) +### Code Quality -
+```bash +# Format code +uv run ruff format . -
-LM Studio +# Lint code +uv run ruff check . -Run models locally with [LM Studio](https://lmstudio.ai). Load a model in the Chat or Developer tab, then set `MODEL` to its identifier. +# Type checking +uv run ty . +``` -Examples (native tool-use support): -- `lmstudio-community/qwen2.5-7b-instruct` -- `lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF` -- `bartowski/Ministral-8B-Instruct-2410-GGUF` +### Development Server -Browse: [model.lmstudio.ai](https://model.lmstudio.ai) +```bash +# Run with auto-reload +uv run uvicorn server:app --host 0.0.0.0 --port 8082 --reload -
+# Run with debug logging +uv run uvicorn server:app --host 0.0.0.0 --port 8082 --log-level debug +``` --- -## Configuration - -| Variable | Description | Default | -|----------|-------------|---------| -| `PROVIDER_TYPE` | Provider: `nvidia_nim`, `open_router`, or `lmstudio` | `nvidia_nim` | -| `MODEL` | Model to use for all requests | `stepfun-ai/step-3.5-flash` | -| `NVIDIA_NIM_API_KEY` | NVIDIA API key (NIM provider) | required | -| `OPENROUTER_API_KEY` | OpenRouter API key (OpenRouter provider) | required | -| `LM_STUDIO_BASE_URL` | LM Studio server URL | `http://localhost:1234/v1` | -| `PROVIDER_RATE_LIMIT` | LLM API requests per window | `40` | -| `PROVIDER_RATE_WINDOW` | Rate limit window (seconds) | `60` | -| `HTTP_READ_TIMEOUT` | Read timeout for provider API requests (seconds) | `300` | -| `HTTP_WRITE_TIMEOUT` | Write timeout for provider API requests (seconds) | `10` | -| `HTTP_CONNECT_TIMEOUT` | Connect timeout for provider API requests (seconds) | `2` | -| `FAST_PREFIX_DETECTION` | Enable fast prefix detection | `true` | -| `ENABLE_NETWORK_PROBE_MOCK` | Enable network probe mock | `true` | -| `ENABLE_TITLE_GENERATION_SKIP` | Skip title generation | `true` | -| `ENABLE_SUGGESTION_MODE_SKIP` | Skip suggestion mode | `true` | -| `ENABLE_FILEPATH_EXTRACTION_MOCK` | Enable filepath extraction mock | `true` | -| `MESSAGING_PLATFORM` | Messaging platform: `discord` or `telegram` | `discord` | -| `DISCORD_BOT_TOKEN` | Discord Bot Token | `""` | -| `ALLOWED_DISCORD_CHANNELS` | Comma-separated channel IDs (empty = none allowed) | `""` | -| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token | `""` | -| `ALLOWED_TELEGRAM_USER_ID` | Allowed Telegram User ID | `""` | -| `VOICE_NOTE_ENABLED` | Enable voice note handling | `true` | -| `WHISPER_MODEL` | Local Whisper model size | `base` | -| `WHISPER_DEVICE` | `cpu` \| `cuda` \| `auto` | `cpu` | -| `MESSAGING_RATE_LIMIT` | Messaging messages per window | `1` | -| `MESSAGING_RATE_WINDOW` | Messaging window (seconds) | `1` | -| `CLAUDE_WORKSPACE` | Directory for agent workspace | `./agent_workspace` | -| `ALLOWED_DIR` | Allowed directories for agent | `""` | -| `MAX_CLI_SESSIONS` | Max concurrent CLI sessions | `10` | - -See [`.env.example`](.env.example) for all supported parameters. +## 🛠️ Troubleshooting ---- +### Common Issues -## Development +#### Port 8082 Already in Use -### Project Structure +```bash +# Windows +netstat -ano | findstr :8082 +taskkill /PID /F +# Linux/Mac +lsof -ti:8082 | xargs kill -9 ``` -free-claude-code/ -├── server.py # Entry point -├── api/ # FastAPI routes, request detection, optimization handlers -├── providers/ # BaseProvider, OpenAICompatibleProvider, NIM, OpenRouter, LM Studio -│ └── common/ # Shared utils (SSE builder, message converter, parsers, error mapping) -├── messaging/ # MessagingPlatform ABC + Discord/Telegram bots, session management -├── config/ # Settings, NIM config, logging -├── cli/ # CLI session and process management -├── utils/ # Text utilities -└── tests/ # Pytest test suite -``` -### Commands +#### API Key Invalid + +- NVIDIA NIM keys start with `nvapi-` +- OpenRouter keys start with `sk-or-` +- Check for typos and trailing spaces + +#### VSCode Extension Not Connecting + +1. Verify server is running: `pm2 list` +2. Test connection: `curl http://localhost:8082/health` +3. Check VSCode settings are correct +4. Reload VSCode window + +#### Model Not Found ```bash -uv run ruff format # Format code -uv run ruff check # Code style checking -uv run ty check # Type checking -uv run pytest # Run tests +# List available models +curl http://localhost:8082/v1/models + +# Check nvidia_nim_models.json for valid model IDs ``` +For more troubleshooting help, see [SETUP_GUIDE.md](SETUP_GUIDE.md). + --- -## Extending +## 📚 Documentation -### Adding a Provider +- **[SETUP_GUIDE.md](SETUP_GUIDE.md)** - Comprehensive setup and configuration guide +- **[setup/README.md](setup/README.md)** - Setup wizard internal documentation +- **[.env.example](.env.example)** - Configuration template with all options +- **[LICENSE](LICENSE)** - MIT License details -For **OpenAI-compatible APIs** (Groq, Together AI, etc.), extend `OpenAICompatibleProvider`: +--- -```python -from providers.openai_compat import OpenAICompatibleProvider -from providers.base import ProviderConfig +## 🤝 Contributing -class MyProvider(OpenAICompatibleProvider): - def __init__(self, config: ProviderConfig): - super().__init__(config, provider_name="MYPROVIDER", - base_url="https://api.example.com/v1", api_key=config.api_key) +Contributions are welcome! This is an open fork designed to be accessible to everyone. - def _build_request_body(self, request): - return build_request_body(request) # Your request builder -``` +### How to Contribute -For **fully custom APIs**, extend `BaseProvider` directly: +1. **Fork the repository** +2. **Create a feature branch**: `git checkout -b feature/amazing-feature` +3. **Make your changes** +4. **Run tests**: `uv run pytest` +5. **Format code**: `uv run ruff format .` +6. **Commit changes**: `git commit -m 'Add amazing feature'` +7. **Push to branch**: `git push origin feature/amazing-feature` +8. **Open a Pull Request** -```python -from providers.base import BaseProvider, ProviderConfig +### Areas for Contribution -class MyProvider(BaseProvider): - async def stream_response(self, request, input_tokens=0, *, request_id=None): - # Yield Anthropic SSE format events - ... -``` +- 🌐 **Mac/Linux Setup Wizard** - Port the Windows wizard to other platforms +- 🔌 **New Providers** - Add support for more AI providers +- 💬 **Messaging Platforms** - Add Slack, WhatsApp, etc. +- 📖 **Documentation** - Improve guides and add tutorials +- 🐛 **Bug Fixes** - Fix issues and improve stability +- ✨ **Features** - Add new capabilities -### Adding a Messaging Platform +### Code Style -Extend `MessagingPlatform` in `messaging/` to add Slack or other platforms: +- Follow PEP 8 for Python code +- Use type hints +- Write tests for new features +- Update documentation -```python -from messaging.base import MessagingPlatform +--- + +## 📜 License -class MyPlatform(MessagingPlatform): - async def start(self): - # Initialize connection - ... +This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. - async def stop(self): - # Cleanup - ... +``` +MIT License + +Copyright (c) 2026 Ali Khokhar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` - async def send_message(self, chat_id, text, reply_to=None, parse_mode=None): - # Send a message - ... +--- - async def edit_message(self, chat_id, message_id, text, parse_mode=None): - # Edit an existing message - ... +## 🙏 Acknowledgments - def on_message(self, handler): - # Register callback for incoming messages - ... -``` +- **Original Project**: [Alishahryar1/free-claude-code](https://github.com/Alishahryar1/free-claude-code) - The foundation this fork builds upon +- **Anthropic**: For creating Claude and the Claude Code CLI +- **NVIDIA**: For providing free NIM API access +- **Community Contributors**: Everyone who has contributed to making this project better --- -## Contributing +## 🔗 Links -Contributions are welcome! Here are some ways to help: +- **Original Repository**: [github.com/Alishahryar1/free-claude-code](https://github.com/Alishahryar1/free-claude-code) +- **This Fork**: [github.com/rishiskhare/free-claude-code](https://github.com/rishiskhare/free-claude-code) +- **Claude Code CLI**: [github.com/anthropics/claude-code](https://github.com/anthropics/claude-code) +- **NVIDIA NIM**: [build.nvidia.com](https://build.nvidia.com) +- **OpenRouter**: [openrouter.ai](https://openrouter.ai) +- **LM Studio**: [lmstudio.ai](https://lmstudio.ai) -- Report bugs or suggest features via [Issues](https://github.com/Alishahryar1/free-claude-code/issues) -- Add new LLM providers (Groq, Together AI, etc.) -- Add new messaging platforms (Slack, etc.) -- Improve test coverage +--- -```bash -# Fork the repo, then: -git checkout -b my-feature -# Make your changes -uv run ruff format && uv run ruff check && uv run ty check && uv run pytest -# Open a pull request -``` +## ⭐ Star History + +If you find this project useful, please consider giving it a star! ⭐ --- -## License +## 📞 Support + +- **Issues**: [github.com/rishiskhare/free-claude-code/issues](https://github.com/rishiskhare/free-claude-code/issues) +- **Discussions**: Use GitHub Discussions for questions and ideas +- **Documentation**: Check [SETUP_GUIDE.md](SETUP_GUIDE.md) for detailed help -This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file for details. +--- -Built with [FastAPI](https://fastapi.tiangolo.com/), [OpenAI Python SDK](https://github.com/openai/openai-python), [discord.py](https://github.com/Rapptz/discord.py), and [python-telegram-bot](https://python-telegram-bot.org/). +
+ +**Made with ❤️ by the community** + +**Happy coding with free Claude! 🚀** + +
diff --git a/SETUP.bat b/SETUP.bat new file mode 100644 index 00000000..db367497 --- /dev/null +++ b/SETUP.bat @@ -0,0 +1,64 @@ +@echo off +REM ======================================================================== +REM Free Claude Code - One-Click Setup +REM Double-click this file to install everything automatically! +REM ======================================================================== + +title Free Claude Code - Setup Wizard + +echo. +echo ======================================================================== +echo FREE CLAUDE CODE - AUTOMATED SETUP +echo ======================================================================== +echo. +echo This wizard will automatically: +echo - Install all prerequisites (Python, Node.js, PM2, etc.) +echo - Configure your AI provider +echo - Set up the proxy server +echo - Create desktop shortcuts +echo. +echo Press any key to start, or close this window to cancel... +echo. +pause >nul + +REM Check for PowerShell +where powershell >nul 2>nul +if %ERRORLEVEL% NEQ 0 ( + echo. + echo ERROR: PowerShell not found! + echo Please install PowerShell to continue. + echo. + pause + exit /b 1 +) + +REM Launch the setup wizard +echo. +echo Starting setup wizard... +echo. +powershell -ExecutionPolicy Bypass -File "%~dp0setup\Setup-Wizard.ps1" + +if %ERRORLEVEL% EQU 0 ( + echo. + echo ======================================================================== + echo SETUP COMPLETE! + echo ======================================================================== + echo. + echo Your proxy server is now running at: http://localhost:8082 + echo. + echo Next steps: + echo 1. Configure VSCode extension (see README.md) + echo 2. Test: curl http://localhost:8082/v1/models + echo 3. Manage server: pm2 list / pm2 logs free-claude-code + echo. +) else ( + echo. + echo ======================================================================== + echo SETUP FAILED + echo ======================================================================== + echo. + echo Check setup.log for details. + echo. +) + +pause diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 00000000..532634f8 --- /dev/null +++ b/SETUP_GUIDE.md @@ -0,0 +1,530 @@ +# Free Claude Code - Setup Guide + +Complete guide for installing and configuring the Free Claude Code proxy server. + +## Table of Contents + +- [Quick Start](#quick-start) +- [System Requirements](#system-requirements) +- [Installation Methods](#installation-methods) +- [Configuration](#configuration) +- [Troubleshooting](#troubleshooting) +- [Advanced Usage](#advanced-usage) +- [FAQ](#faq) + +--- + +## Quick Start + +### Automated Installation (Recommended) + +1. **Download the repository** + ```bash + git clone https://github.com/rishiskhare/free-claude-code.git + cd free-claude-code + ``` + +2. **Run the installer** + - **Windows**: Double-click `INSTALL.bat` or run: + ```powershell + .\setup\Setup-Wizard.ps1 + ``` + +3. **Follow the wizard** + - Select your AI provider (NVIDIA NIM, OpenRouter, or LM Studio) + - Enter your API key (if required) + - Choose a model + - Configure optional features + +4. **Done!** The server will start automatically. + +--- + +## System Requirements + +### Minimum Requirements + +| Component | Requirement | +|-----------|-------------| +| **OS** | Windows 10/11 (64-bit) | +| **RAM** | 4 GB minimum, 8 GB recommended | +| **Disk Space** | 2 GB free space | +| **Internet** | Required for NVIDIA NIM and OpenRouter | + +### Required Software + +The setup wizard will automatically install these if missing: + +- **Python 3.14+** - Runtime for the proxy server +- **Node.js 18+** - Required for PM2 process manager +- **uv** - Python package manager +- **PM2** - Process manager for background service +- **fzf** - Fuzzy finder for model selection + +### Optional Software + +- **Claude Code CLI** - For terminal usage +- **VSCode** - For VSCode extension integration + +--- + +## Installation Methods + +### Method 1: Automated Wizard (Recommended) + +**Full Interactive Installation:** +```powershell +.\setup\Setup-Wizard.ps1 +``` + +**Silent Installation (with existing .env):** +```powershell +.\setup\Setup-Wizard.ps1 -Silent +``` + +**Skip Specific Steps:** +```powershell +# Skip shortcuts +.\setup\Setup-Wizard.ps1 -SkipShortcuts + +# Skip validation +.\setup\Setup-Wizard.ps1 -SkipValidation + +# Skip PM2 setup +.\setup\Setup-Wizard.ps1 -SkipPM2 +``` + +**Repair Mode:** +```powershell +.\setup\Setup-Wizard.ps1 -Mode repair +``` + +### Method 2: Manual Installation + +If you prefer manual setup or the wizard fails: + +1. **Install Prerequisites** + ```powershell + # Install Python 3.14+ + # Download from: https://www.python.org/downloads/ + + # Install Node.js + # Download from: https://nodejs.org/ + + # Install uv + pip install uv + + # Install PM2 + npm install -g pm2 + + # Install fzf + # Download from: https://github.com/junegunn/fzf/releases + ``` + +2. **Setup Python Environment** + ```powershell + cd free-claude-code + uv sync + ``` + +3. **Configure Environment** + ```powershell + cp .env.example .env + # Edit .env with your settings + ``` + +4. **Start Server** + ```powershell + pm2 start "uv run uvicorn server:app --host 0.0.0.0 --port 8082" --name "free-claude-code" + ``` + +--- + +## Configuration + +### Provider Setup + +#### NVIDIA NIM (Free, Recommended) + +1. Get API key: https://build.nvidia.com/settings/api-keys +2. Configure: + ```env + PROVIDER_TYPE=nvidia_nim + NVIDIA_NIM_API_KEY=nvapi-your-key-here + MODEL=stepfun-ai/step-3.5-flash + ``` + +**Popular Models:** +- `stepfun-ai/step-3.5-flash` - Fast, balanced +- `deepseek-ai/deepseek-v3.1` - High quality +- `meta/llama-3.3-70b-instruct` - Strong reasoning +- `moonshotai/kimi-k2.5` - Long context + +#### OpenRouter (Paid) + +1. Get API key: https://openrouter.ai/keys +2. Configure: + ```env + PROVIDER_TYPE=open_router + OPENROUTER_API_KEY=sk-or-your-key-here + MODEL=anthropic/claude-3.5-sonnet + ``` + +#### LM Studio (Local) + +1. Install LM Studio: https://lmstudio.ai/ +2. Load a model and start the server +3. Configure: + ```env + PROVIDER_TYPE=lmstudio + LM_STUDIO_BASE_URL=http://localhost:1234/v1 + MODEL=local-model + ``` + +### VSCode Extension Setup + +1. Install the Claude Code extension +2. Open VSCode Settings (Ctrl+,) +3. Search for `claude-code.environmentVariables` +4. Add: + ```json + "claude-code.environmentVariables": [ + { "name": "ANTHROPIC_BASE_URL", "value": "http://localhost:8082" }, + { "name": "ANTHROPIC_AUTH_TOKEN", "value": "freecc" } + ] + ``` +5. Reload VSCode + +**Use Specific Model:** +```json +{ "name": "ANTHROPIC_AUTH_TOKEN", "value": "freecc:moonshotai/kimi-k2.5" } +``` + +### Optional Features + +#### Discord Bot + +```env +MESSAGING_PLATFORM=discord +DISCORD_BOT_TOKEN=your-bot-token +ALLOWED_DISCORD_CHANNELS=channel-id-1,channel-id-2 +``` + +#### Telegram Bot + +```env +MESSAGING_PLATFORM=telegram +TELEGRAM_BOT_TOKEN=your-bot-token +ALLOWED_TELEGRAM_USER_ID=your-user-id +``` + +#### Voice Transcription + +```env +VOICE_NOTE_ENABLED=true +WHISPER_MODEL=base +WHISPER_DEVICE=cpu +HF_TOKEN=your-huggingface-token # Optional +``` + +--- + +## Troubleshooting + +### Common Issues + +#### 1. PowerShell Execution Policy Error + +**Error:** "cannot be loaded because running scripts is disabled" + +**Solution:** +```powershell +Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +``` + +#### 2. Port 8082 Already in Use + +**Error:** "Address already in use" + +**Solution:** +```powershell +# Find process using port 8082 +netstat -ano | findstr :8082 + +# Kill the process (replace PID) +taskkill /PID /F + +# Or change port in .env +# Add: PORT=8083 +``` + +#### 3. Python Not Found After Installation + +**Solution:** +```powershell +# Refresh environment variables +$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + +# Or restart PowerShell +``` + +#### 4. PM2 Service Won't Start + +**Check logs:** +```powershell +pm2 logs free-claude-code +``` + +**Common fixes:** +```powershell +# Restart service +pm2 restart free-claude-code + +# Delete and recreate +pm2 delete free-claude-code +pm2 start ecosystem.config.js + +# Check Python path in ecosystem.config.js +``` + +#### 5. API Key Invalid + +**NVIDIA NIM:** +- Keys start with `nvapi-` +- Get new key: https://build.nvidia.com/settings/api-keys +- Check for typos + +**OpenRouter:** +- Keys start with `sk-or-` +- Verify at: https://openrouter.ai/keys + +#### 6. Model Not Found + +**Solution:** +```powershell +# List available models +curl http://localhost:8082/v1/models + +# Check nvidia_nim_models.json for valid model IDs +``` + +#### 7. VSCode Extension Not Connecting + +**Checklist:** +1. Server running? `pm2 list` +2. Port accessible? `curl http://localhost:8082/health` +3. Environment variables set correctly? +4. VSCode reloaded after config change? + +**Test connection:** +```powershell +curl http://localhost:8082/v1/models -H "Authorization: Bearer freecc" +``` + +### Getting Help + +1. **Check logs:** + ```powershell + # Setup log + type setup.log + + # Server logs + pm2 logs free-claude-code + ``` + +2. **Verify installation:** + ```powershell + .\setup\Setup-Wizard.ps1 -Mode repair -SkipValidation:$false + ``` + +3. **Report issues:** + - GitHub: https://github.com/rishiskhare/free-claude-code/issues + - Include: OS version, error message, setup.log + +--- + +## Advanced Usage + +### Command-Line Options + +```powershell +# Full help +.\setup\Setup-Wizard.ps1 -ShowHelp + +# Repair installation +.\setup\Setup-Wizard.ps1 -Mode repair + +# Update installation +.\setup\Setup-Wizard.ps1 -Mode update + +# Force reinstall +.\setup\Setup-Wizard.ps1 -Force + +# Custom log path +.\setup\Setup-Wizard.ps1 -LogPath "C:\logs\setup.log" +``` + +### PM2 Management + +```powershell +# List all services +pm2 list + +# View logs +pm2 logs free-claude-code + +# Restart service +pm2 restart free-claude-code + +# Stop service +pm2 stop free-claude-code + +# Start service +pm2 start free-claude-code + +# Monitor resources +pm2 monit + +# Save configuration +pm2 save + +# Setup auto-start on boot +pm2 startup +``` + +### Manual Server Start + +```powershell +# Without PM2 +cd free-claude-code +uv run uvicorn server:app --host 0.0.0.0 --port 8082 + +# With custom port +uv run uvicorn server:app --host 0.0.0.0 --port 8083 + +# With reload (development) +uv run uvicorn server:app --host 0.0.0.0 --port 8082 --reload +``` + +### Environment Variables + +All settings can be configured in `.env`: + +```env +# Provider Configuration +PROVIDER_TYPE=nvidia_nim +PROVIDER_RATE_LIMIT=40 +PROVIDER_RATE_WINDOW=60 + +# HTTP Timeouts +HTTP_READ_TIMEOUT=300 +HTTP_WRITE_TIMEOUT=10 +HTTP_CONNECT_TIMEOUT=2 + +# Server Configuration +HOST=0.0.0.0 +PORT=8082 + +# Agent Configuration +CLAUDE_WORKSPACE=./agent_workspace +MAX_CLI_SESSIONS=10 +ALLOWED_DIR=/path/to/allowed/directory + +# Optimization Flags +FAST_PREFIX_DETECTION=true +ENABLE_NETWORK_PROBE_MOCK=true +ENABLE_TITLE_GENERATION_SKIP=true +ENABLE_SUGGESTION_MODE_SKIP=true +ENABLE_FILEPATH_EXTRACTION_MOCK=true +``` + +### Multiple Instances + +Run multiple instances with different ports: + +```powershell +# Instance 1 (port 8082) +pm2 start ecosystem.config.js --name "claude-nvidia" + +# Instance 2 (port 8083) +# Edit ecosystem.config.js to use port 8083 +pm2 start ecosystem.config.js --name "claude-openrouter" +``` + +--- + +## FAQ + +### General + +**Q: Is this really free?** +A: Yes! NVIDIA NIM provides 40 free requests per minute. OpenRouter has free models too. + +**Q: Do I need an Anthropic API key?** +A: No! That's the whole point - you use NVIDIA NIM, OpenRouter, or LM Studio instead. + +**Q: Can I use this with the official Claude Code?** +A: Yes! Just point it to `http://localhost:8082` instead of Anthropic's API. + +**Q: Does this work on Mac/Linux?** +A: The setup wizard is Windows-only, but you can manually install on Mac/Linux. + +### Performance + +**Q: Which model is fastest?** +A: `stepfun-ai/step-3.5-flash` is very fast. `deepseek-v3.1` is slower but higher quality. + +**Q: Can I use multiple models?** +A: Yes! Change the `MODEL` in `.env` and restart: `pm2 restart free-claude-code` + +**Q: What's the rate limit?** +A: NVIDIA NIM: 40 req/min. OpenRouter: varies by model. LM Studio: unlimited. + +### Troubleshooting + +**Q: Setup wizard fails to install Python** +A: Download manually from python.org and run wizard again with `-SkipPrereqs` + +**Q: Server starts but VSCode can't connect** +A: Check firewall settings. Try `curl http://localhost:8082/health` + +**Q: How do I update?** +A: Run `.\setup\Setup-Wizard.ps1 -Mode update` + +**Q: How do I uninstall?** +A: Run `.\UNINSTALL.ps1` + +### Advanced + +**Q: Can I run this on a remote server?** +A: Yes! Change `HOST=0.0.0.0` in `.env` and configure firewall. + +**Q: Can I use a custom model?** +A: Yes! Set `MODEL=your-model-id` in `.env` + +**Q: How do I enable HTTPS?** +A: Use a reverse proxy like nginx or Caddy. + +--- + +## Support + +- **Documentation**: README.md, SETUP_GUIDE.md +- **Issues**: https://github.com/rishiskhare/free-claude-code/issues +- **Logs**: `setup.log`, `pm2 logs free-claude-code` + +--- + +## Next Steps + +After successful installation: + +1. ✅ Test the API: `curl http://localhost:8082/v1/models` +2. ✅ Configure VSCode extension +3. ✅ Try the CLI: `claude-free` +4. ✅ Read the main README.md for usage examples +5. ✅ Join the community and share feedback! + +--- + +**Happy coding with free Claude! 🚀** diff --git a/UNINSTALL.ps1 b/UNINSTALL.ps1 new file mode 100644 index 00000000..239a45f6 --- /dev/null +++ b/UNINSTALL.ps1 @@ -0,0 +1,184 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Uninstaller for Free Claude Code +.DESCRIPTION + Removes Free Claude Code installation and optionally cleans up dependencies +.PARAMETER RemoveVenv + Remove Python virtual environment +.PARAMETER RemoveShortcuts + Remove desktop and startup shortcuts +.PARAMETER RemovePM2 + Stop and remove PM2 service +.PARAMETER RemoveConfig + Remove .env configuration file +.PARAMETER KeepLogs + Keep log files +.EXAMPLE + .\UNINSTALL.ps1 + Interactive uninstallation +.EXAMPLE + .\UNINSTALL.ps1 -RemoveVenv -RemoveShortcuts -RemovePM2 + Remove everything except config +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [switch]$RemoveVenv, + + [Parameter(Mandatory = $false)] + [switch]$RemoveShortcuts, + + [Parameter(Mandatory = $false)] + [switch]$RemovePM2, + + [Parameter(Mandatory = $false)] + [switch]$RemoveConfig, + + [Parameter(Mandatory = $false)] + [switch]$KeepLogs, + + [Parameter(Mandatory = $false)] + [switch]$Silent +) + +$ErrorActionPreference = 'Continue' +$ProjectRoot = $PSScriptRoot + +function Read-Confirmation { + param([string]$Prompt, [switch]$DefaultYes) + if ($Silent) { return $true } + $default = if ($DefaultYes) { 'Y' } else { 'n' } + $response = Read-Host "$Prompt [$default]" + if ([string]::IsNullOrWhiteSpace($response)) { return $DefaultYes.IsPresent } + return $response -match '^[Yy]' +} + +Write-Host "" +Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Red +Write-Host " Free Claude Code - Uninstaller" -ForegroundColor Red +Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Red +Write-Host "" +Write-Host "This will remove Free Claude Code from your system." -ForegroundColor Yellow +Write-Host "" + +if (-not (Read-Confirmation "Continue with uninstallation?" -DefaultYes:$false)) { + Write-Host "Uninstallation cancelled." -ForegroundColor Yellow + exit 0 +} + +Write-Host "" +Write-Host "Uninstalling..." -ForegroundColor Cyan +Write-Host "" + +# Stop and remove PM2 service +if ($RemovePM2 -or (Read-Confirmation "Stop and remove PM2 service?" -DefaultYes:$true)) { + Write-Host "[⟳] Stopping PM2 service..." -ForegroundColor Cyan + + $pm2 = Get-Command pm2 -ErrorAction SilentlyContinue + if ($pm2) { + & pm2 stop free-claude-code 2>&1 | Out-Null + & pm2 delete free-claude-code 2>&1 | Out-Null + & pm2 save 2>&1 | Out-Null + Write-Host "[✓] PM2 service removed" -ForegroundColor Green + } else { + Write-Host "[ℹ] PM2 not found, skipping" -ForegroundColor Gray + } +} + +# Remove configuration files +$filesToRemove = @( + 'ecosystem.config.js' +) + +if ($RemoveConfig -or (Read-Confirmation "Remove .env configuration?" -DefaultYes:$false)) { + $filesToRemove += '.env' +} + +Write-Host "[⟳] Removing configuration files..." -ForegroundColor Cyan +foreach ($file in $filesToRemove) { + $path = Join-Path $ProjectRoot $file + if (Test-Path $path) { + Remove-Item $path -Force -ErrorAction SilentlyContinue + Write-Host " [✓] Removed: $file" -ForegroundColor Gray + } +} + +# Remove backup files +$backups = Get-ChildItem -Path $ProjectRoot -Filter "*.backup.*" -ErrorAction SilentlyContinue +foreach ($backup in $backups) { + Remove-Item $backup.FullName -Force -ErrorAction SilentlyContinue + Write-Host " [✓] Removed backup: $($backup.Name)" -ForegroundColor Gray +} + +# Remove virtual environment +if ($RemoveVenv -or (Read-Confirmation "Remove Python virtual environment (.venv)?" -DefaultYes:$true)) { + Write-Host "[⟳] Removing virtual environment..." -ForegroundColor Cyan + $venvPath = Join-Path $ProjectRoot ".venv" + if (Test-Path $venvPath) { + Remove-Item $venvPath -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "[✓] Virtual environment removed" -ForegroundColor Green + } else { + Write-Host "[ℹ] Virtual environment not found" -ForegroundColor Gray + } +} + +# Remove shortcuts +if ($RemoveShortcuts -or (Read-Confirmation "Remove shortcuts?" -DefaultYes:$true)) { + Write-Host "[⟳] Removing shortcuts..." -ForegroundColor Cyan + + $desktop = [Environment]::GetFolderPath('Desktop') + $startup = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup" + $startMenu = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs" + + $shortcuts = @( + (Join-Path $desktop "Start Claude Proxy.lnk"), + (Join-Path $startup "Claude-Proxy.lnk"), + (Join-Path $startMenu "Free Claude Code") + ) + + foreach ($shortcut in $shortcuts) { + if (Test-Path $shortcut) { + Remove-Item $shortcut -Recurse -Force -ErrorAction SilentlyContinue + Write-Host " [✓] Removed: $shortcut" -ForegroundColor Gray + } + } + + Write-Host "[✓] Shortcuts removed" -ForegroundColor Green +} + +# Remove logs +if (-not $KeepLogs -and (Read-Confirmation "Remove log files?" -DefaultYes:$false)) { + Write-Host "[⟳] Removing logs..." -ForegroundColor Cyan + + $logsPath = Join-Path $ProjectRoot "logs" + if (Test-Path $logsPath) { + Remove-Item $logsPath -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "[✓] Logs removed" -ForegroundColor Green + } + + $setupLog = Join-Path $ProjectRoot "setup.log" + if (Test-Path $setupLog) { + Remove-Item $setupLog -Force -ErrorAction SilentlyContinue + Remove-Item "$setupLog.*" -Force -ErrorAction SilentlyContinue + } +} + +Write-Host "" +Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Green +Write-Host " Uninstallation Complete" -ForegroundColor Green +Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Green +Write-Host "" +Write-Host "Free Claude Code has been uninstalled." -ForegroundColor White +Write-Host "" +Write-Host "Note: System prerequisites (Python, Node.js, PM2, etc.) were NOT removed." -ForegroundColor Gray +Write-Host "You can manually uninstall them if no longer needed." -ForegroundColor Gray +Write-Host "" +Write-Host "You can safely delete this folder: $ProjectRoot" -ForegroundColor Gray +Write-Host "" + +if (-not $Silent) { + Read-Host "Press Enter to exit" +} diff --git a/api/app.py b/api/app.py index 3ee8e02b..a43b37ab 100644 --- a/api/app.py +++ b/api/app.py @@ -189,6 +189,11 @@ def create_app() -> FastAPI: # Register routes app.include_router(router) + # Register custom fork routes (claude-free model picker) + from .custom_routes import router as custom_router + + app.include_router(custom_router) + # Exception handlers @app.exception_handler(ProviderError) async def provider_error_handler(request: Request, exc: ProviderError): diff --git a/api/custom_routes.py b/api/custom_routes.py new file mode 100644 index 00000000..6fefc445 --- /dev/null +++ b/api/custom_routes.py @@ -0,0 +1,44 @@ +"""Custom routes for free-claude-code fork. + +This module contains custom endpoints that are specific to this fork +(claude-free model picker, per-session model overrides). +These are kept separate from upstream code to avoid merge conflicts. +""" + +import json +from pathlib import Path +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +router = APIRouter() + +MODELS_FILE = Path(__file__).resolve().parent.parent / "nvidia_nim_models.json" + + +def _parse_model_override(api_key: str) -> str | None: + """Extract model override from auth token. + + Format: "freecc:org/model-name" → returns "org/model-name" + "freecc" or "freecc:" → returns None (use default from settings) + """ + if ":" not in api_key: + return None + _, model = api_key.split(":", 1) + return model.strip() or None + + +@router.get("/v1/models") +async def list_models(): + """Return available NVIDIA NIM models from nvidia_nim_models.json. + + Used by the claude-free model picker script. + """ + try: + data = json.loads(MODELS_FILE.read_text()) + return JSONResponse(content=data) + except FileNotFoundError: + raise HTTPException(status_code=404, detail="nvidia_nim_models.json not found") + except json.JSONDecodeError: + raise HTTPException( + status_code=500, detail="Invalid JSON in nvidia_nim_models.json" + ) diff --git a/api/routes.py b/api/routes.py index fcf7d4c3..88e6bfd0 100644 --- a/api/routes.py +++ b/api/routes.py @@ -21,6 +21,19 @@ router = APIRouter() +def _parse_model_override(raw_request: Request) -> str | None: + """Extract model override from x-api-key header. + + Format: "freecc:org/model-name" → returns "org/model-name" + "freecc" or "freecc:" → returns None (use default) + """ + api_key = raw_request.headers.get("x-api-key", "") + if ":" not in api_key: + return None + _, model = api_key.split(":", 1) + return model.strip() or None + + # ============================================================================= # Routes # ============================================================================= @@ -36,6 +49,14 @@ async def create_message( """Create a message (always streaming).""" try: + # Per-session model override via auth token (freecc:org/model-name) + model_override = _parse_model_override(raw_request) + if model_override: + logger.info( + f"Model override via token: {request_data.model} -> {model_override}" + ) + request_data.model = model_override + optimized = try_optimizations(request_data, settings) if optimized is not None: return optimized diff --git a/claude-free b/claude-free new file mode 100755 index 00000000..140bd11f --- /dev/null +++ b/claude-free @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# claude-free — Interactive model picker for free-claude-code +# Usage: ./claude-free [extra claude args...] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODELS_FILE="$SCRIPT_DIR/nvidia_nim_models.json" +PORT="${CLAUDE_FREE_PORT:-8082}" +BASE_URL="http://localhost:$PORT" + +if [[ ! -f "$MODELS_FILE" ]]; then + echo "Error: $MODELS_FILE not found" >&2 + exit 1 +fi + +# Extract model IDs from the JSON file +models=$(python3 -c " +import json, sys +data = json.load(open('$MODELS_FILE')) +for m in data.get('data', []): + print(m['id']) +" 2>/dev/null) + +if [[ -z "$models" ]]; then + echo "Error: No models found in $MODELS_FILE" >&2 + exit 1 +fi + +# Pick a model: use fzf if available, otherwise a numbered menu +if command -v fzf &>/dev/null; then + model=$(echo "$models" | fzf --prompt="Select a model> " --height=40% --reverse) +else + echo "Select a model:" + IFS=$'\n' read -rd '' -a model_arr <<< "$models" || true + select model in "${model_arr[@]}"; do + [[ -n "$model" ]] && break + echo "Invalid selection, try again." + done +fi + +if [[ -z "${model:-}" ]]; then + echo "No model selected." >&2 + exit 1 +fi + +echo "Launching Claude Code with model: $model" +ANTHROPIC_AUTH_TOKEN="freecc:$model" ANTHROPIC_BASE_URL="$BASE_URL" claude "$@" diff --git a/claude-free.bat b/claude-free.bat new file mode 100644 index 00000000..5582e295 --- /dev/null +++ b/claude-free.bat @@ -0,0 +1,38 @@ +@echo off +REM claude-free.bat - Interactive model picker for free-claude-code on Windows + +set SCRIPT_DIR=%~dp0 +set MODELS_FILE=%SCRIPT_DIR%nvidia_nim_models.json +set PORT=8082 +set BASE_URL=http://localhost:%PORT% + +if not exist "%MODELS_FILE%" ( + echo Error: %MODELS_FILE% not found + exit /b 1 +) + +REM Extract model IDs from JSON using Python +for /f "delims=" %%i in ('python -c "import json; data=json.load(open('%MODELS_FILE%')); [print(m['id']) for m in data.get('data', [])]" 2^>nul') do ( + set models=%%i +) + +REM Use fzf if available, otherwise show numbered menu +where fzf >nul 2>&1 +if %errorlevel% equ 0 ( + for /f "delims=" %%i in ('python -c "import json; data=json.load(open('%MODELS_FILE%')); [print(m['id']) for m in data.get('data', [])]" ^| fzf --prompt="Select a model> " --height=40%% --reverse') do set model=%%i +) else ( + echo Select a model: + python -c "import json; data=json.load(open('%MODELS_FILE%')); [print(f'{i+1}. {m[\"id\"]}') for i, m in enumerate(data.get('data', []))]" + set /p choice="Enter number: " + for /f "delims=" %%i in ('python -c "import json; data=json.load(open('%MODELS_FILE%')); print(data['data'][int('%choice%')-1]['id'])"') do set model=%%i +) + +if "%model%"=="" ( + echo No model selected. + exit /b 1 +) + +echo Launching Claude Code with model: %model% +set ANTHROPIC_AUTH_TOKEN=freecc:%model% +set ANTHROPIC_BASE_URL=%BASE_URL% +claude %* diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 00000000..0311f7c6 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,15 @@ +module.exports = { + apps: [{ + name: 'claude-proxy', + script: 'C:\\Users\\Administrator\\.local\\bin\\uv.exe', + args: 'run uvicorn server:app --host 0.0.0.0 --port 8082', + cwd: 'C:\\Users\\Administrator\\Downloads\\free-claude-code', + interpreter: 'none', + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'production' + } + }] +}; diff --git a/fork/claude-free b/fork/claude-free new file mode 100755 index 00000000..140bd11f --- /dev/null +++ b/fork/claude-free @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# claude-free — Interactive model picker for free-claude-code +# Usage: ./claude-free [extra claude args...] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODELS_FILE="$SCRIPT_DIR/nvidia_nim_models.json" +PORT="${CLAUDE_FREE_PORT:-8082}" +BASE_URL="http://localhost:$PORT" + +if [[ ! -f "$MODELS_FILE" ]]; then + echo "Error: $MODELS_FILE not found" >&2 + exit 1 +fi + +# Extract model IDs from the JSON file +models=$(python3 -c " +import json, sys +data = json.load(open('$MODELS_FILE')) +for m in data.get('data', []): + print(m['id']) +" 2>/dev/null) + +if [[ -z "$models" ]]; then + echo "Error: No models found in $MODELS_FILE" >&2 + exit 1 +fi + +# Pick a model: use fzf if available, otherwise a numbered menu +if command -v fzf &>/dev/null; then + model=$(echo "$models" | fzf --prompt="Select a model> " --height=40% --reverse) +else + echo "Select a model:" + IFS=$'\n' read -rd '' -a model_arr <<< "$models" || true + select model in "${model_arr[@]}"; do + [[ -n "$model" ]] && break + echo "Invalid selection, try again." + done +fi + +if [[ -z "${model:-}" ]]; then + echo "No model selected." >&2 + exit 1 +fi + +echo "Launching Claude Code with model: $model" +ANTHROPIC_AUTH_TOKEN="freecc:$model" ANTHROPIC_BASE_URL="$BASE_URL" claude "$@" diff --git a/scripts/sync_readme.py b/scripts/sync_readme.py new file mode 100644 index 00000000..5e43182d --- /dev/null +++ b/scripts/sync_readme.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +import re +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +UPSTREAM_REF = os.environ.get("UPSTREAM_REF", "upstream/main") +TOP_NOTE = "> Based on [Alishahryar1/free-claude-code](https://github.com/Alishahryar1/free-claude-code). This fork adds simplified setup and easy custom model selection." +QUICK_START_SECTION = """## Quick Start (5 minutes) + +### Step 1: Install the prerequisites + +You need these before starting: + +| What | Where to get it | +| --- | --- | +| NVIDIA API key (free) | [build.nvidia.com/settings/api-keys](https://build.nvidia.com/settings/api-keys) | +| Claude Code CLI | [github.com/anthropics/claude-code](https://github.com/anthropics/claude-code) | +| uv (Python package runner) | [github.com/astral-sh/uv](https://github.com/astral-sh/uv) | +| PM2 (keeps the proxy running) | `npm install -g pm2` | +| fzf (fuzzy model picker) | [github.com/junegunn/fzf](https://github.com/junegunn/fzf) | + + +### Step 2: Clone the repo and add your API key + +```bash +git clone https://github.com/rishiskhare/free-claude-code.git +cd free-claude-code +cp .env.example .env +``` + +Now open `.env` and set the `NVIDIA_NIM_API_KEY` value: + +```dotenv +NVIDIA_NIM_API_KEY="nvapi-paste-your-key-here" +``` + +You only need to change that one key to get started. + +> **Want to use a different provider?** See [Providers](#providers) for OpenRouter (hundreds of models) or LM Studio (fully local). + +### Step 3: Start the proxy server + +```bash +pm2 start "uv run uvicorn server:app --host 0.0.0.0 --port 8082" --name "claude-proxy" +``` + +That's it - the proxy is now running in the background. You can close this terminal and it keeps going. Use these commands to manage it: + +| Command | What it does | +| --- | --- | +| `pm2 logs claude-proxy` | See server logs (useful for troubleshooting) | +| `pm2 stop claude-proxy` | Stop the proxy | +| `pm2 restart claude-proxy` | Restart it (e.g., after editing `.env`) | +| `pm2 list` | Check if the proxy is running | + +### Step 4: Launch Claude Code + +#### Option A: Terminal (CLI) + +Add this alias to your `~/.zshrc` (macOS) or `~/.bashrc` (Linux): + +```bash +alias claude-free='/full/path/to/free-claude-code/claude-free' +``` + +Replace the path with where you cloned the repo (e.g., `/Users/yourname/Downloads/free-claude-code/`), then reload your shell: + +```bash +source ~/.zshrc # or: source ~/.bashrc +``` + +Now you can run it from any directory: + +```bash +claude-free +``` + +You'll see a searchable list of every available model. Pick one and go. Just type a few letters to filter (e.g., type "kimi" to find Kimi K2.5 instantly). + +#### Option B: VSCode Extension + +If you use the [Claude Code VSCode extension](https://marketplace.visualstudio.com/items?itemName=anthropics.claude-code), you can point it at the proxy too: + +1. Open VSCode Settings (`Cmd + ,` on macOS, `Ctrl + ,` on Linux/Windows). +2. Search for `claude-code.environmentVariables`. +3. Click **Edit in settings.json** and add: + +```json +"claude-code.environmentVariables": [ + { "name": "ANTHROPIC_BASE_URL", "value": "http://localhost:8082" }, + { "name": "ANTHROPIC_AUTH_TOKEN", "value": "freecc" } +] +``` + +4. Reload the extension (or restart VSCode). +5. **If you see the login screen** ("How do you want to log in?"): Click **Anthropic Console**, then authorize. The extension will start working. You may be redirected to buy credits in the browser - ignore that; the extension already works. + +That's it - the Claude Code panel in VSCode now uses NVIDIA NIM for free. To switch back to Anthropic, remove or comment out the block above and reload. + +> **Tip:** To use a specific model from VSCode, set the token to `freecc:model-id` (e.g., `"freecc:moonshotai/kimi-k2.5"`). Otherwise it uses the `MODEL` value from your `.env`. +""" +MODEL_ALIASES_SECTION = """## Model-Specific Aliases (Optional) + +You can also create aliases that skip the picker and go straight into a specific model. Add this to your `~/.zshrc` or `~/.bashrc`: + +```bash +alias claude-kimi='ANTHROPIC_BASE_URL="http://localhost:8082" ANTHROPIC_AUTH_TOKEN="freecc:moonshotai/kimi-k2.5" claude' +``` + +Swap out the model ID after `freecc:` to use any model. Then run `source ~/.zshrc` (or `source ~/.bashrc`). +""" + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def git_show(ref: str, relpath: str) -> str | None: + try: + return subprocess.check_output( + ["git", "show", f"{ref}:{relpath}"], + cwd=ROOT, + text=True, + ) + except subprocess.CalledProcessError: + return None + + +def ensure_top_note(text: str, note: str) -> str: + if note.strip() in text: + return text + match = re.search(r"^# .+$", text, re.M) + if match: + insert_at = match.end() + return text[:insert_at] + "\n\n" + note.strip() + "\n" + text[insert_at:] + return note.strip() + "\n\n" + text + + +def replace_section(text: str, heading_pattern: str, new_section: str) -> str: + pattern = re.compile( + rf"^{heading_pattern}\n.*?(?=^##\s+|\Z)", + re.S | re.M, + ) + new_block = new_section.strip() + "\n\n" + if pattern.search(text): + return pattern.sub(new_block, text, count=1) + return text.rstrip() + "\n\n" + new_block + + +def main() -> None: + upstream_readme = git_show(UPSTREAM_REF, "README.md") + if upstream_readme is None: + upstream_readme = read_text(ROOT / "README.md") + + updated = ensure_top_note(upstream_readme, TOP_NOTE) + updated = replace_section(updated, r"##\s+Quick Start.*", QUICK_START_SECTION) + updated = replace_section( + updated, r"##\s+Model-Specific Aliases.*", MODEL_ALIASES_SECTION + ) + + (ROOT / "README.md").write_text(updated.rstrip() + "\n", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/setup/README.md b/setup/README.md new file mode 100644 index 00000000..5509903b --- /dev/null +++ b/setup/README.md @@ -0,0 +1,36 @@ +# Setup Wizard - Internal Structure + +This directory contains the automated setup wizard for Free Claude Code. + +## Files + +- **`Setup-Wizard.ps1`** - Main orchestrator script +- **`modules/`** - PowerShell modules (10 files) +- **`assets/`** - ASCII art logo +- **`backups/`** - Temporary backup storage during installation + +## Usage + +Users should run `SETUP.bat` from the root directory, not these files directly. + +## For Developers + +To modify the setup wizard: + +1. Edit modules in `modules/` directory +2. Test with: `.\Setup-Wizard.ps1 -SkipValidation` +3. Check logs in `../setup.log` + +## Modules + +| Module | Purpose | +|--------|---------| +| Logger.psm1 | Logging system | +| ProgressDisplay.psm1 | Retro UI | +| UIModules.psm1 | UI components | +| RollbackManager.psm1 | Change tracking | +| PrerequisiteChecker.psm1 | Dependency installation | +| ConfigWizard.psm1 | Configuration wizard | +| ShortcutManager.psm1 | Shortcut creation | +| PM2Manager.psm1 | PM2 service setup | +| Validator.psm1 | Post-install checks | diff --git a/setup/Setup-Wizard.ps1 b/setup/Setup-Wizard.ps1 new file mode 100644 index 00000000..c203e847 --- /dev/null +++ b/setup/Setup-Wizard.ps1 @@ -0,0 +1,475 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Free Claude Code Setup Wizard - Main Orchestrator +.DESCRIPTION + Automated installation wizard for Free Claude Code proxy server +.PARAMETER Mode + Installation mode: install, repair, or update +.PARAMETER Silent + Run in silent mode (no interactive prompts) +.PARAMETER SkipPrereqs + Skip prerequisite checks +.PARAMETER SkipShortcuts + Skip shortcut creation +.PARAMETER SkipPM2 + Skip PM2 service setup +.PARAMETER SkipValidation + Skip post-install validation +.PARAMETER Force + Force reinstallation +.PARAMETER LogPath + Custom log file path +.EXAMPLE + .\Setup-Wizard.ps1 + Run full interactive installation +.EXAMPLE + .\Setup-Wizard.ps1 -Mode repair + Repair existing installation +.EXAMPLE + .\Setup-Wizard.ps1 -SkipShortcuts -SkipValidation + Install without shortcuts and validation +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [ValidateSet('install', 'repair', 'update')] + [string]$Mode = 'install', + + [Parameter(Mandatory = $false)] + [switch]$Silent, + + [Parameter(Mandatory = $false)] + [switch]$SkipPrereqs, + + [Parameter(Mandatory = $false)] + [switch]$SkipShortcuts, + + [Parameter(Mandatory = $false)] + [switch]$SkipPM2, + + [Parameter(Mandatory = $false)] + [switch]$SkipValidation, + + [Parameter(Mandatory = $false)] + [switch]$Force, + + [Parameter(Mandatory = $false)] + [string]$LogPath, + + [Parameter(Mandatory = $false)] + [Alias('h', 'help')] + [switch]$ShowHelp +) + +# ═══════════════════════════════════════════════════════════════════ +# HELP +# ═══════════════════════════════════════════════════════════════════ + +if ($ShowHelp) { + Write-Host @" + +Free Claude Code Setup Wizard v2.0.0 + +USAGE: + .\Setup-Wizard.ps1 [OPTIONS] + +OPTIONS: + -Mode Installation mode (default: install) + -Silent Run without interactive prompts + -SkipPrereqs Skip prerequisite installation + -SkipShortcuts Skip desktop/startup shortcuts + -SkipPM2 Skip PM2 service setup + -SkipValidation Skip post-install validation + -Force Force reinstallation + -LogPath Custom log file path + -ShowHelp, -h, -help Show this help message + +EXAMPLES: + .\Setup-Wizard.ps1 + Full interactive installation + + .\Setup-Wizard.ps1 -Mode repair + Repair existing installation + + .\Setup-Wizard.ps1 -SkipShortcuts + Install without creating shortcuts + +For more information, see SETUP_GUIDE.md + +"@ + exit 0 +} + +# ═══════════════════════════════════════════════════════════════════ +# INITIALIZATION +# ═══════════════════════════════════════════════════════════════════ + +$ErrorActionPreference = 'Stop' +$ProjectRoot = Resolve-Path "$PSScriptRoot\.." + +# Import modules +Import-Module "$PSScriptRoot\modules\Logger.psm1" -Force +Import-Module "$PSScriptRoot\modules\ProgressDisplay.psm1" -Force +Import-Module "$PSScriptRoot\modules\RollbackManager.psm1" -Force +Import-Module "$PSScriptRoot\modules\UIModules.psm1" -Force +Import-Module "$PSScriptRoot\modules\PrerequisiteChecker.psm1" -Force +Import-Module "$PSScriptRoot\modules\ConfigWizard.psm1" -Force +Import-Module "$PSScriptRoot\modules\ShortcutManager.psm1" -Force +Import-Module "$PSScriptRoot\modules\PM2Manager.psm1" -Force +Import-Module "$PSScriptRoot\modules\Validator.psm1" -Force + +# Initialize logger +if (-not $LogPath) { + $LogPath = Join-Path $ProjectRoot "setup.log" +} +Initialize-Logger -LogPath $LogPath + +Write-Log "═══════════════════════════════════════════════════════════════════" -Level INFO +Write-Log "Starting Setup Wizard (Mode: $Mode)" -Level INFO +Write-Log "Project root: $ProjectRoot" -Level INFO +Write-Log "PowerShell version: $($PSVersionTable.PSVersion)" -Level INFO +Write-Log "═══════════════════════════════════════════════════════════════════" -Level INFO + +# ═══════════════════════════════════════════════════════════════════ +# MAIN EXECUTION +# ═══════════════════════════════════════════════════════════════════ + +try { + Clear-Host + Show-RetroLogo + Write-Host "" + + # Check admin rights + if (-not (Test-IsAdmin)) { + Write-Host "" + Write-Status "Running without administrator privileges" -Level Warning + Write-Host " Some operations may fail without admin rights." -ForegroundColor Yellow + Write-Host " Consider running as Administrator for best results." -ForegroundColor Yellow + Write-Host "" + + if (-not $Silent) { + Start-Sleep -Seconds 2 + } + } + + Write-Log "Admin check completed" -Level INFO + + # ═══════════════════════════════════════════════════════════════ + # PHASE 1: PREREQUISITES + # ═══════════════════════════════════════════════════════════════ + + if (-not $SkipPrereqs -and $Mode -ne 'repair') { + Show-StepIndicator -Current 1 -Total 6 -StepName "Checking Prerequisites" + Write-Log "Phase 1: Checking prerequisites" -Level INFO + + $prereqs = Test-Prerequisites + + Write-Host "Prerequisite Status:" -ForegroundColor Cyan + Write-Host "" + + foreach ($key in $prereqs.Keys) { + if ($key -eq 'AllRequiredPass') { continue } + + $item = $prereqs[$key] + if ($item.Installed) { + Write-Status "$key $($item.Version) [OK]" -Level Success + } else { + $status = if ($item.Required) { "REQUIRED" } else { "OPTIONAL" } + Write-Status "$key [$status - $($item.Status)]" -Level Warning + } + } + + Write-Host "" + + if (-not $prereqs.AllRequiredPass) { + Write-Log "Missing prerequisites detected" -Level WARN + + if ($Silent -or (Read-Confirmation "Install missing prerequisites?" -DefaultYes:$true)) { + Invoke-PrerequisiteInstallation -Results $prereqs + Write-Log "Prerequisites installed successfully" -Level INFO + } else { + Write-Host "Skipping prerequisite installation. Setup may fail." -ForegroundColor Yellow + Write-Log "User skipped prerequisite installation" -Level WARN + } + } else { + Write-Status "All prerequisites satisfied!" -Level Success + Write-Log "All prerequisites satisfied" -Level INFO + } + + Write-Host "" + if (-not $Silent) { + Read-Host "Press Enter to continue" + } + } + + # ═══════════════════════════════════════════════════════════════ + # PHASE 2: PYTHON ENVIRONMENT + # ═══════════════════════════════════════════════════════════════ + + if ($Mode -in 'install', 'update') { + Show-StepIndicator -Current 2 -Total 6 -StepName "Setting up Python Environment" + Write-Log "Phase 2: Setting up Python environment" -Level INFO + + Write-Status "Installing Python dependencies..." -Level Processing + + Push-Location $ProjectRoot + try { + $output = & uv sync 2>&1 + + if ($LASTEXITCODE -ne 0) { + throw "uv sync failed: $output" + } + + Write-Status "Python environment ready" -Level Success + Write-Log "Python dependencies installed" -Level INFO + } catch { + Write-Log "uv sync failed: $_" -Level ERROR + throw "Failed to set up Python environment: $_" + } finally { + Pop-Location + } + + Write-Host "" + if (-not $Silent) { + Read-Host "Press Enter to continue" + } + } + + # ═══════════════════════════════════════════════════════════════ + # PHASE 3: CONFIGURATION + # ═══════════════════════════════════════════════════════════════ + + if ($Mode -ne 'repair' -or $Force) { + Show-StepIndicator -Current 3 -Total 6 -StepName "Configuration" + Write-Log "Phase 3: Configuration wizard" -Level INFO + + if (-not $Silent) { + $config = Get-UserConfiguration -ProjectRoot $ProjectRoot + + if ($null -eq $config) { + throw "Configuration cancelled by user" + } + + # Backup existing .env + $envPath = Join-Path $ProjectRoot ".env" + if (Test-Path $envPath) { + $backup = "$envPath.backup.$(Get-Date -Format 'yyyyMMdd_HHmmss')" + Copy-Item $envPath $backup -Force + Register-Change -Action CreateFile -Path $backup + Write-Log "Backed up existing .env to: $backup" -Level INFO + } + + # Write new .env + New-EnvironmentFile -Config $config -OutputPath $envPath + Register-Change -Action CreateFile -Path $envPath + + Write-Host "" + Write-Status "Configuration saved to .env" -Level Success + Write-Log "Configuration file created" -Level INFO + } else { + Write-Host "Silent mode: Skipping configuration (using existing .env)" -ForegroundColor Yellow + Write-Log "Silent mode: Skipped configuration" -Level WARN + } + + Write-Host "" + if (-not $Silent) { + Read-Host "Press Enter to continue" + } + } + + # ═══════════════════════════════════════════════════════════════ + # PHASE 4: SHORTCUTS + # ═══════════════════════════════════════════════════════════════ + + if (-not $SkipShortcuts) { + Show-StepIndicator -Current 4 -Total 6 -StepName "Creating Shortcuts" + Write-Log "Phase 4: Creating shortcuts" -Level INFO + + $batPath = Join-Path $ProjectRoot "start-claude-proxy.bat" + + # Check if bat file exists, if not use alternative + if (-not (Test-Path $batPath)) { + $batPath = Join-Path $ProjectRoot "claude-free.bat" + } + + if (Test-Path $batPath) { + if ($Silent -or (Read-Confirmation "Create desktop shortcut?" -DefaultYes:$true)) { + try { + $shortcut = New-DesktopShortcut -Target $batPath -ShortcutName "Start Claude Proxy" -WorkingDirectory $ProjectRoot + Write-Status "Desktop shortcut created" -Level Success + Write-Log "Desktop shortcut created: $shortcut" -Level INFO + } catch { + Write-Status "Failed to create desktop shortcut: $_" -Level Warning + Write-Log "Desktop shortcut failed: $_" -Level WARN + } + } + + if ($Silent -or (Read-Confirmation "Create startup shortcut (auto-start on login)?" -DefaultYes:$false)) { + try { + $shortcut = New-StartupShortcut -Target $batPath -ShortcutName "Claude-Proxy" -WorkingDirectory $ProjectRoot + Write-Status "Startup shortcut created" -Level Success + Write-Log "Startup shortcut created: $shortcut" -Level INFO + } catch { + Write-Status "Failed to create startup shortcut: $_" -Level Warning + Write-Log "Startup shortcut failed: $_" -Level WARN + } + } + } else { + Write-Status "Batch file not found, skipping shortcuts" -Level Warning + Write-Log "Batch file not found at: $batPath" -Level WARN + } + + Write-Host "" + if (-not $Silent) { + Read-Host "Press Enter to continue" + } + } + + # ═══════════════════════════════════════════════════════════════ + # PHASE 5: PM2 SERVICE + # ═══════════════════════════════════════════════════════════════ + + if (-not $SkipPM2 -and $Mode -ne 'repair') { + Show-StepIndicator -Current 5 -Total 6 -StepName "Configuring PM2 Service" + Write-Log "Phase 5: Configuring PM2 service" -Level INFO + + Write-Status "Generating PM2 ecosystem config..." -Level Processing + + # Read .env file + $envPath = Join-Path $ProjectRoot ".env" + if (Test-Path $envPath) { + $envContent = Get-Content $envPath + $envConfig = @{} + + foreach ($line in $envContent) { + if ($line -match '^([^#=]+)=(.*)$') { + $key = $matches[1].Trim() + $value = $matches[2].Trim().Trim('"') + $envConfig[$key] = $value + } + } + + $ecosystemPath = Join-Path $ProjectRoot "ecosystem.config.js" + New-PM2EcosystemConfig -Config $envConfig -OutputPath $ecosystemPath -ProjectRoot $ProjectRoot + Write-Status "PM2 config created" -Level Success + Write-Log "PM2 ecosystem config created" -Level INFO + + if ($Silent -or (Read-Confirmation "Start PM2 service now?" -DefaultYes:$true)) { + try { + Register-PM2Service -ProjectRoot $ProjectRoot + Write-Status "PM2 service started" -Level Success + Write-Log "PM2 service started" -Level INFO + + Write-Host "" + Write-Host " Server is now running at: http://localhost:8082" -ForegroundColor Green + Write-Host " Use 'pm2 logs free-claude-code' to view logs" -ForegroundColor Gray + Write-Host "" + } catch { + Write-Status "Failed to start PM2 service: $_" -Level Warning + Write-Log "PM2 service start failed: $_" -Level WARN + } + } + } else { + Write-Status ".env file not found, skipping PM2 setup" -Level Warning + Write-Log ".env file not found" -Level WARN + } + + Write-Host "" + if (-not $Silent) { + Read-Host "Press Enter to continue" + } + } + + # ═══════════════════════════════════════════════════════════════ + # PHASE 6: VALIDATION + # ═══════════════════════════════════════════════════════════════ + + if (-not $SkipValidation) { + Show-StepIndicator -Current 6 -Total 6 -StepName "Validation" + Write-Log "Phase 6: Post-install validation" -Level INFO + + Write-Status "Running post-install checks..." -Level Processing + Write-Host "" + + $validation = Test-Installation -ProjectRoot $ProjectRoot + Show-VerificationReport -Results $validation + + Write-Log "Validation completed. AllPass: $($validation.AllPass)" -Level INFO + + if (-not $validation.AllPass -and -not $Silent) { + if (-not (Read-Confirmation "Some checks failed. Continue anyway?" -DefaultYes:$true)) { + throw "Validation failed" + } + } + + if (-not $Silent) { + Read-Host "Press Enter to continue" + } + } + + # ═══════════════════════════════════════════════════════════════ + # COMPLETION + # ═══════════════════════════════════════════════════════════════ + + Clear-Host + Show-RetroLogo + Write-Host "" + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Green + Write-Host " Setup Complete!" -ForegroundColor Green + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Green + Write-Host "" + Write-Host "Your free-claude-code server is ready to use!" -ForegroundColor White + Write-Host "" + Write-Host "Quick Start:" -ForegroundColor Cyan + Write-Host " • Start server: " -NoNewline -ForegroundColor Gray + Write-Host "pm2 start free-claude-code" -ForegroundColor White + Write-Host " • View logs: " -NoNewline -ForegroundColor Gray + Write-Host "pm2 logs free-claude-code" -ForegroundColor White + Write-Host " • Stop server: " -NoNewline -ForegroundColor Gray + Write-Host "pm2 stop free-claude-code" -ForegroundColor White + Write-Host " • API docs: " -NoNewline -ForegroundColor Gray + Write-Host "http://localhost:8082/docs" -ForegroundColor White + Write-Host "" + Write-Host "Next Steps:" -ForegroundColor Cyan + Write-Host " 1. Configure VSCode extension (see README.md)" -ForegroundColor Gray + Write-Host " 2. Test API: curl http://localhost:8082/v1/models" -ForegroundColor Gray + Write-Host " 3. Read documentation: SETUP_GUIDE.md" -ForegroundColor Gray + Write-Host "" + Write-Host "Logs:" -ForegroundColor Cyan + Write-Host " • Setup log: $LogPath" -ForegroundColor Gray + Write-Host "" + + Write-Log "Setup completed successfully" -Level INFO + Close-Logger + + # Clear rollback stack on success + Clear-RollbackStack + +} catch { + Write-Host "" + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Red + Write-Host " SETUP FAILED" -ForegroundColor Red + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Red + Write-Host "" + Write-Host "Error: $_" -ForegroundColor Red + Write-Host "" + Write-Host "Check the log file for details: $LogPath" -ForegroundColor Yellow + Write-Host "" + + Write-Log "Setup failed: $_" -Level ERROR + Write-Log "Stack trace: $($_.ScriptStackTrace)" -Level ERROR + + if (-not $Silent) { + $rollback = Read-Confirmation "Attempt to rollback changes?" -DefaultYes:$true + if ($rollback) { + Write-Host "" + Invoke-Rollback + } + } + + Close-Logger + exit 1 +} diff --git a/setup/assets/logo.txt b/setup/assets/logo.txt new file mode 100644 index 00000000..b3702a91 --- /dev/null +++ b/setup/assets/logo.txt @@ -0,0 +1,19 @@ + ╔═══════════════════════════════════════════════════════════════════╗ + ║ ║ + ║ ███████╗██████╗ ███████╗███████╗ ██████╗██╗ █████╗ ║ + ║ ██╔════╝██╔══██╗██╔════╝██╔════╝ ██╔════╝██║ ██╔══██╗ ║ + ║ █████╗ ██████╔╝█████╗ █████╗ ██║ ██║ ███████║ ║ + ║ ██╔══╝ ██╔══██╗██╔══╝ ██╔══╝ ██║ ██║ ██╔══██║ ║ + ║ ██║ ██║ ██║███████╗███████╗ ╚██████╗███████╗██║ ██║ ║ + ║ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ║ + ║ ║ + ║ ██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ ║ + ║ ██║ ██║██╔══██╗██╔════╝ ██╔════╝██╔═══██╗ ║ + ║ ██║ ██║██║ ██║█████╗ ██║ ██║ ██║ ║ + ║ ██║ ██║██║ ██║██╔══╝ ██║ ██║ ██║ ║ + ║ ╚██████╔╝██████╔╝███████╗ ╚██████╗╚██████╔╝ ║ + ║ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ║ + ║ ║ + ║ Free Claude Code Setup Wizard ║ + ║ v2.0.0 | Automated Installer ║ + ╚═══════════════════════════════════════════════════════════════════╝ diff --git a/setup/modules/ConfigWizard.psm1 b/setup/modules/ConfigWizard.psm1 new file mode 100644 index 00000000..7af8dc06 --- /dev/null +++ b/setup/modules/ConfigWizard.psm1 @@ -0,0 +1,499 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Interactive configuration wizard +.DESCRIPTION + Guides user through configuration and generates .env file +#> + +function Get-UserConfiguration { + <# + .SYNOPSIS + Run interactive configuration wizard + .PARAMETER ProjectRoot + Root directory of the project + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ProjectRoot + ) + + $config = @{} + + Write-Host "" + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host " Configuration Wizard" -ForegroundColor Cyan + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host "" + + # Step 1: Provider Selection + $provider = Show-ProviderSelection + $config['PROVIDER_TYPE'] = $provider + + # Step 2: Provider-specific configuration + switch ($provider) { + 'nvidia_nim' { + $config += Get-NVIDIAConfig -ProjectRoot $ProjectRoot + } + 'open_router' { + $config += Get-OpenRouterConfig + } + 'lmstudio' { + $config += Get-LMStudioConfig + } + } + + # Step 3: Optional features + Write-Host "" + Write-Host "Optional Features" -ForegroundColor Cyan + Write-Host "─────────────────────────────────────────────────────────────────" -ForegroundColor DarkGray + Write-Host "" + + # Discord/Telegram bot + if (Read-Confirmation "Enable Discord bot?" -DefaultYes:$false) { + $config += Get-DiscordConfig + } + + if (Read-Confirmation "Enable Telegram bot?" -DefaultYes:$false) { + $config += Get-TelegramConfig + } + + # Voice transcription + if (Read-Confirmation "Enable voice note transcription?" -DefaultYes:$false) { + $config['VOICE_NOTE_ENABLED'] = 'true' + $config['WHISPER_MODEL'] = 'base' + $config['WHISPER_DEVICE'] = 'cpu' + + $hfToken = Read-Host "Hugging Face token (optional, press Enter to skip)" + if (-not [string]::IsNullOrWhiteSpace($hfToken)) { + $config['HF_TOKEN'] = $hfToken + } + } else { + $config['VOICE_NOTE_ENABLED'] = 'false' + } + + # Step 4: Advanced settings (with defaults) + Write-Host "" + if (Read-Confirmation "Configure advanced settings?" -DefaultYes:$false) { + $config += Get-AdvancedConfig + } else { + # Use defaults + $config['PROVIDER_RATE_LIMIT'] = '40' + $config['PROVIDER_RATE_WINDOW'] = '60' + $config['HTTP_READ_TIMEOUT'] = '300' + $config['HTTP_WRITE_TIMEOUT'] = '10' + $config['HTTP_CONNECT_TIMEOUT'] = '2' + $config['CLAUDE_WORKSPACE'] = './agent_workspace' + $config['MAX_CLI_SESSIONS'] = '10' + $config['FAST_PREFIX_DETECTION'] = 'true' + $config['ENABLE_NETWORK_PROBE_MOCK'] = 'true' + $config['ENABLE_TITLE_GENERATION_SKIP'] = 'true' + $config['ENABLE_SUGGESTION_MODE_SKIP'] = 'true' + $config['ENABLE_FILEPATH_EXTRACTION_MOCK'] = 'true' + } + + # Step 5: Review configuration + Write-Host "" + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host " Configuration Summary" -ForegroundColor Cyan + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host "" + + foreach ($key in ($config.Keys | Sort-Object)) { + $value = $config[$key] + # Mask sensitive values + if ($key -match 'KEY|TOKEN|PASSWORD') { + $maskedValue = if ($value.Length -gt 8) { + $value.Substring(0, 4) + ('*' * ($value.Length - 8)) + $value.Substring($value.Length - 4) + } else { + '****' + } + Write-Host " $key = $maskedValue" -ForegroundColor Gray + } else { + Write-Host " $key = $value" -ForegroundColor Gray + } + } + + Write-Host "" + + if (-not (Read-Confirmation "Save this configuration?" -DefaultYes:$true)) { + Write-Host "Configuration cancelled." -ForegroundColor Yellow + return $null + } + + return $config +} + +function Show-ProviderSelection { + <# + .SYNOPSIS + Display provider selection menu + #> + [CmdletBinding()] + param() + + Write-Host "Select AI Provider:" -ForegroundColor Cyan + Write-Host "" + Write-Host " 1) NVIDIA NIM (Free, 40 req/min)" -ForegroundColor Gray + Write-Host " 2) OpenRouter (Many models, paid)" -ForegroundColor Gray + Write-Host " 3) LM Studio (Local, no API key)" -ForegroundColor Gray + Write-Host "" + + do { + $choice = Read-Host "Choice [1-3]" + + switch ($choice) { + '1' { return 'nvidia_nim' } + '2' { return 'open_router' } + '3' { return 'lmstudio' } + default { + Write-Host "Please enter 1, 2, or 3" -ForegroundColor Yellow + } + } + } while ($true) +} + +function Get-NVIDIAConfig { + <# + .SYNOPSIS + Get NVIDIA NIM configuration + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ProjectRoot + ) + + $config = @{} + + Write-Host "" + Write-Host "NVIDIA NIM Configuration" -ForegroundColor Cyan + Write-Host "─────────────────────────────────────────────────────────────────" -ForegroundColor DarkGray + Write-Host "" + Write-Host "Get your free API key at: https://build.nvidia.com/settings/api-keys" -ForegroundColor Yellow + Write-Host "" + + do { + $apiKey = Read-Host "NVIDIA NIM API Key" + + if ([string]::IsNullOrWhiteSpace($apiKey)) { + Write-Host "API key is required for NVIDIA NIM" -ForegroundColor Red + continue + } + + if ($apiKey -notmatch '^nvapi-') { + Write-Host "Warning: NVIDIA API keys usually start with 'nvapi-'" -ForegroundColor Yellow + if (-not (Read-Confirmation "Continue anyway?" -DefaultYes:$false)) { + continue + } + } + + break + } while ($true) + + $config['NVIDIA_NIM_API_KEY'] = $apiKey + + # Model selection + Write-Host "" + Write-Host "Select a model:" -ForegroundColor Cyan + Write-Host "" + + $modelsFile = Join-Path $ProjectRoot "nvidia_nim_models.json" + if (Test-Path $modelsFile) { + $models = Get-Content $modelsFile | ConvertFrom-Json + $modelList = $models.data | Select-Object -ExpandProperty id + + # Show popular models + $popularModels = @( + 'stepfun-ai/step-3.5-flash', + 'deepseek-ai/deepseek-v3.1', + 'meta/llama-3.3-70b-instruct', + 'mistralai/mistral-large-3-675b-instruct-2512', + 'moonshotai/kimi-k2.5' + ) + + Write-Host "Popular models:" -ForegroundColor Gray + for ($i = 0; $i -lt $popularModels.Count; $i++) { + Write-Host " $($i + 1)) $($popularModels[$i])" -ForegroundColor Gray + } + Write-Host " 6) Enter custom model ID" -ForegroundColor Gray + Write-Host " 7) View all $($modelList.Count) available models" -ForegroundColor Gray + Write-Host "" + + do { + $choice = Read-Host "Choice [1-7]" + + if ($choice -match '^\d+$') { + $choiceNum = [int]$choice + if ($choiceNum -ge 1 -and $choiceNum -le 5) { + $config['MODEL'] = $popularModels[$choiceNum - 1] + break + } elseif ($choiceNum -eq 6) { + $customModel = Read-Host "Enter model ID" + if ($modelList -contains $customModel) { + $config['MODEL'] = $customModel + break + } else { + Write-Host "Warning: Model not in list. Using anyway." -ForegroundColor Yellow + $config['MODEL'] = $customModel + break + } + } elseif ($choiceNum -eq 7) { + Write-Host "" + Write-Host "All available models:" -ForegroundColor Cyan + $modelList | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + Write-Host "" + continue + } + } + + Write-Host "Please enter a number between 1 and 7" -ForegroundColor Yellow + } while ($true) + } else { + $config['MODEL'] = Read-Host "Model ID [stepfun-ai/step-3.5-flash]" + if ([string]::IsNullOrWhiteSpace($config['MODEL'])) { + $config['MODEL'] = 'stepfun-ai/step-3.5-flash' + } + } + + return $config +} + +function Get-OpenRouterConfig { + <# + .SYNOPSIS + Get OpenRouter configuration + #> + [CmdletBinding()] + param() + + $config = @{} + + Write-Host "" + Write-Host "OpenRouter Configuration" -ForegroundColor Cyan + Write-Host "─────────────────────────────────────────────────────────────────" -ForegroundColor DarkGray + Write-Host "" + Write-Host "Get your API key at: https://openrouter.ai/keys" -ForegroundColor Yellow + Write-Host "" + + $apiKey = Read-Host "OpenRouter API Key" + $config['OPENROUTER_API_KEY'] = $apiKey + + $model = Read-Host "Model ID [anthropic/claude-3.5-sonnet]" + if ([string]::IsNullOrWhiteSpace($model)) { + $model = 'anthropic/claude-3.5-sonnet' + } + $config['MODEL'] = $model + + return $config +} + +function Get-LMStudioConfig { + <# + .SYNOPSIS + Get LM Studio configuration + #> + [CmdletBinding()] + param() + + $config = @{} + + Write-Host "" + Write-Host "LM Studio Configuration" -ForegroundColor Cyan + Write-Host "─────────────────────────────────────────────────────────────────" -ForegroundColor DarkGray + Write-Host "" + Write-Host "Make sure LM Studio is running with a model loaded." -ForegroundColor Yellow + Write-Host "" + + $baseUrl = Read-Host "LM Studio Base URL [http://localhost:1234/v1]" + if ([string]::IsNullOrWhiteSpace($baseUrl)) { + $baseUrl = 'http://localhost:1234/v1' + } + $config['LM_STUDIO_BASE_URL'] = $baseUrl + + $model = Read-Host "Model name [local-model]" + if ([string]::IsNullOrWhiteSpace($model)) { + $model = 'local-model' + } + $config['MODEL'] = $model + + return $config +} + +function Get-DiscordConfig { + <# + .SYNOPSIS + Get Discord bot configuration + #> + [CmdletBinding()] + param() + + $config = @{} + + Write-Host "" + Write-Host "Discord Bot Configuration" -ForegroundColor Cyan + Write-Host "─────────────────────────────────────────────────────────────────" -ForegroundColor DarkGray + Write-Host "" + + $config['MESSAGING_PLATFORM'] = 'discord' + $config['DISCORD_BOT_TOKEN'] = Read-Host "Discord Bot Token" + $config['ALLOWED_DISCORD_CHANNELS'] = Read-Host "Allowed Channel IDs (comma-separated)" + + return $config +} + +function Get-TelegramConfig { + <# + .SYNOPSIS + Get Telegram bot configuration + #> + [CmdletBinding()] + param() + + $config = @{} + + Write-Host "" + Write-Host "Telegram Bot Configuration" -ForegroundColor Cyan + Write-Host "─────────────────────────────────────────────────────────────────" -ForegroundColor DarkGray + Write-Host "" + + $config['MESSAGING_PLATFORM'] = 'telegram' + $config['TELEGRAM_BOT_TOKEN'] = Read-Host "Telegram Bot Token" + $config['ALLOWED_TELEGRAM_USER_ID'] = Read-Host "Allowed User ID" + + return $config +} + +function Get-AdvancedConfig { + <# + .SYNOPSIS + Get advanced configuration settings + #> + [CmdletBinding()] + param() + + $config = @{} + + Write-Host "" + Write-Host "Advanced Settings" -ForegroundColor Cyan + Write-Host "─────────────────────────────────────────────────────────────────" -ForegroundColor DarkGray + Write-Host "" + + $rateLimit = Read-Host "Provider rate limit (requests/window) [40]" + $config['PROVIDER_RATE_LIMIT'] = if ([string]::IsNullOrWhiteSpace($rateLimit)) { '40' } else { $rateLimit } + + $rateWindow = Read-Host "Rate limit window (seconds) [60]" + $config['PROVIDER_RATE_WINDOW'] = if ([string]::IsNullOrWhiteSpace($rateWindow)) { '60' } else { $rateWindow } + + $readTimeout = Read-Host "HTTP read timeout (seconds) [300]" + $config['HTTP_READ_TIMEOUT'] = if ([string]::IsNullOrWhiteSpace($readTimeout)) { '300' } else { $readTimeout } + + $workspace = Read-Host "Claude workspace directory [./agent_workspace]" + $config['CLAUDE_WORKSPACE'] = if ([string]::IsNullOrWhiteSpace($workspace)) { './agent_workspace' } else { $workspace } + + # Use defaults for other settings + $config['HTTP_WRITE_TIMEOUT'] = '10' + $config['HTTP_CONNECT_TIMEOUT'] = '2' + $config['MAX_CLI_SESSIONS'] = '10' + $config['FAST_PREFIX_DETECTION'] = 'true' + $config['ENABLE_NETWORK_PROBE_MOCK'] = 'true' + $config['ENABLE_TITLE_GENERATION_SKIP'] = 'true' + $config['ENABLE_SUGGESTION_MODE_SKIP'] = 'true' + $config['ENABLE_FILEPATH_EXTRACTION_MOCK'] = 'true' + + return $config +} + +function New-EnvironmentFile { + <# + .SYNOPSIS + Generate .env file from configuration + .PARAMETER Config + Configuration hashtable + .PARAMETER OutputPath + Path to write the .env file + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Config, + + [Parameter(Mandatory = $true)] + [string]$OutputPath + ) + + try { + $lines = @() + $lines += "# Free Claude Code Configuration" + $lines += "# Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + $lines += "" + + # Group settings + $groups = @{ + 'Provider' = @('PROVIDER_TYPE', 'PROVIDER_RATE_LIMIT', 'PROVIDER_RATE_WINDOW', 'MODEL') + 'HTTP' = @('HTTP_READ_TIMEOUT', 'HTTP_WRITE_TIMEOUT', 'HTTP_CONNECT_TIMEOUT') + 'NVIDIA' = @('NVIDIA_NIM_API_KEY') + 'OpenRouter' = @('OPENROUTER_API_KEY') + 'LMStudio' = @('LM_STUDIO_BASE_URL') + 'Messaging' = @('MESSAGING_PLATFORM', 'MESSAGING_RATE_LIMIT', 'MESSAGING_RATE_WINDOW') + 'Discord' = @('DISCORD_BOT_TOKEN', 'ALLOWED_DISCORD_CHANNELS') + 'Telegram' = @('TELEGRAM_BOT_TOKEN', 'ALLOWED_TELEGRAM_USER_ID') + 'Voice' = @('VOICE_NOTE_ENABLED', 'WHISPER_MODEL', 'WHISPER_DEVICE', 'HF_TOKEN') + 'Agent' = @('CLAUDE_WORKSPACE', 'MAX_CLI_SESSIONS', 'ALLOWED_DIR', 'FAST_PREFIX_DETECTION', + 'ENABLE_NETWORK_PROBE_MOCK', 'ENABLE_TITLE_GENERATION_SKIP', + 'ENABLE_SUGGESTION_MODE_SKIP', 'ENABLE_FILEPATH_EXTRACTION_MOCK') + } + + foreach ($groupName in $groups.Keys) { + $groupKeys = $groups[$groupName] + $hasValues = $false + + foreach ($key in $groupKeys) { + if ($Config.ContainsKey($key)) { + $hasValues = $true + break + } + } + + if ($hasValues) { + $lines += "" + $lines += "# $groupName Configuration" + + foreach ($key in $groupKeys) { + if ($Config.ContainsKey($key)) { + $value = $Config[$key] + $lines += "$key=`"$value`"" + } + } + } + } + + $content = $lines -join "`n" + Set-Content -Path $OutputPath -Value $content -Encoding UTF8 -NoNewline + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "Environment file created at: $OutputPath" -Level INFO + } + + return $true + } catch { + throw "Failed to create environment file: $_" + } +} + +# Import UI functions if available +if (Get-Command Read-Confirmation -ErrorAction SilentlyContinue) { + # Already imported +} else { + function Read-Confirmation { + param([string]$Prompt, [switch]$DefaultYes) + $default = if ($DefaultYes) { 'Y' } else { 'n' } + $response = Read-Host "$Prompt [$default]" + if ([string]::IsNullOrWhiteSpace($response)) { return $DefaultYes.IsPresent } + return $response -match '^[Yy]' + } +} + +Export-ModuleMember -Function Get-UserConfiguration, New-EnvironmentFile, Show-ProviderSelection diff --git a/setup/modules/Logger.psm1 b/setup/modules/Logger.psm1 new file mode 100644 index 00000000..db97a380 --- /dev/null +++ b/setup/modules/Logger.psm1 @@ -0,0 +1,138 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Centralized logging system for the setup wizard +.DESCRIPTION + Provides logging to both console and file with timestamps and log levels +#> + +$script:LogPath = $null +$script:LogInitialized = $false + +function Initialize-Logger { + <# + .SYNOPSIS + Initialize the logging system + .PARAMETER LogPath + Path to the log file + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$LogPath + ) + + $script:LogPath = $LogPath + + # Create log directory if it doesn't exist + $logDir = Split-Path -Path $LogPath -Parent + if ($logDir -and -not (Test-Path $logDir)) { + New-Item -ItemType Directory -Path $logDir -Force | Out-Null + } + + # Rotate old logs (keep last 5) + if (Test-Path $LogPath) { + for ($i = 4; $i -ge 1; $i--) { + $oldLog = "$LogPath.$i" + $newLog = "$LogPath.$($i + 1)" + if (Test-Path $oldLog) { + Move-Item -Path $oldLog -Destination $newLog -Force + } + } + Move-Item -Path $LogPath -Destination "$LogPath.1" -Force + } + + # Write header + $header = @" +================================================================================ +Free Claude Code Setup Wizard - Log File +Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') +PowerShell Version: $($PSVersionTable.PSVersion) +OS: $([System.Environment]::OSVersion.VersionString) +================================================================================ + +"@ + + Set-Content -Path $LogPath -Value $header -Encoding UTF8 + $script:LogInitialized = $true + + Write-Log "Logger initialized at: $LogPath" -Level INFO +} + +function Write-Log { + <# + .SYNOPSIS + Write a log entry + .PARAMETER Message + The message to log + .PARAMETER Level + Log level (INFO, WARN, ERROR, DEBUG) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + + [Parameter(Mandatory = $false)] + [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG')] + [string]$Level = 'INFO' + ) + + if (-not $script:LogInitialized) { + Write-Warning "Logger not initialized. Call Initialize-Logger first." + return + } + + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $logEntry = "[$timestamp] [$Level] $Message" + + # Write to file + try { + Add-Content -Path $script:LogPath -Value $logEntry -Encoding UTF8 + } catch { + Write-Warning "Failed to write to log file: $_" + } +} + +function Get-LogTail { + <# + .SYNOPSIS + Get the last N lines from the log + .PARAMETER Lines + Number of lines to retrieve + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [int]$Lines = 20 + ) + + if (-not $script:LogInitialized -or -not (Test-Path $script:LogPath)) { + return @() + } + + Get-Content -Path $script:LogPath -Tail $Lines +} + +function Close-Logger { + <# + .SYNOPSIS + Finalize the log file + #> + [CmdletBinding()] + param() + + if ($script:LogInitialized) { + $footer = @" + +================================================================================ +Setup completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') +================================================================================ +"@ + Add-Content -Path $script:LogPath -Value $footer -Encoding UTF8 + $script:LogInitialized = $false + } +} + +Export-ModuleMember -Function Initialize-Logger, Write-Log, Get-LogTail, Close-Logger diff --git a/setup/modules/PM2Manager.psm1 b/setup/modules/PM2Manager.psm1 new file mode 100644 index 00000000..b2fcfd1d --- /dev/null +++ b/setup/modules/PM2Manager.psm1 @@ -0,0 +1,250 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + PM2 service manager for the proxy server +.DESCRIPTION + Configures PM2 to manage the proxy server as a background service +#> + +function New-PM2EcosystemConfig { + <# + .SYNOPSIS + Generate PM2 ecosystem.config.js file + .PARAMETER Config + Hashtable of configuration values from .env + .PARAMETER OutputPath + Path to write the config file + .PARAMETER ProjectRoot + Root directory of the project + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Config, + + [Parameter(Mandatory = $true)] + [string]$OutputPath, + + [Parameter(Mandatory = $true)] + [string]$ProjectRoot + ) + + try { + # Determine Python executable path + $pythonExe = Join-Path $ProjectRoot ".venv\Scripts\python.exe" + + if (-not (Test-Path $pythonExe)) { + # Fallback to system Python + $pythonExe = "python" + } + + # Build environment variables object + $envVars = @() + foreach ($key in $Config.Keys) { + $value = $Config[$key] -replace '"', '\"' + $envVars += " ${key}: '$value'" + } + $envString = $envVars -join ",`n" + + # Generate ecosystem config + $ecosystemConfig = @" +module.exports = { + apps: [{ + name: 'free-claude-code', + script: '$($pythonExe -replace '\\', '\\')', + args: '-m uvicorn server:app --host 0.0.0.0 --port 8082', + cwd: '$($ProjectRoot -replace '\\', '\\')', + env: { +$envString + }, + log_file: './logs/combined.log', + out_file: './logs/out.log', + error_file: './logs/error.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true, + autorestart: true, + max_memory_restart: '500M', + watch: false, + instances: 1, + exec_mode: 'fork', + time: true + }] +}; +"@ + + Set-Content -Path $OutputPath -Value $ecosystemConfig -Encoding UTF8 + + if (Get-Command Register-Change -ErrorAction SilentlyContinue) { + Register-Change -Action CreateFile -Path $OutputPath + } + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "PM2 ecosystem config created at: $OutputPath" -Level INFO + } + + return $true + } catch { + throw "Failed to create PM2 ecosystem config: $_" + } +} + +function Register-PM2Service { + <# + .SYNOPSIS + Register and start the PM2 service + .PARAMETER ProjectRoot + Root directory of the project + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ProjectRoot + ) + + try { + $ecosystemPath = Join-Path $ProjectRoot "ecosystem.config.js" + + if (-not (Test-Path $ecosystemPath)) { + throw "Ecosystem config not found at: $ecosystemPath" + } + + # Check if PM2 is available + $pm2 = Get-Command pm2 -ErrorAction SilentlyContinue + if (-not $pm2) { + throw "PM2 not found. Please install PM2 first: npm install -g pm2" + } + + Push-Location $ProjectRoot + + try { + # Stop existing instance if running + & pm2 stop free-claude-code 2>&1 | Out-Null + & pm2 delete free-claude-code 2>&1 | Out-Null + + # Start with ecosystem config + $output = & pm2 start $ecosystemPath 2>&1 + + if ($LASTEXITCODE -ne 0) { + throw "PM2 start failed: $output" + } + + # Save PM2 process list + & pm2 save 2>&1 | Out-Null + + if (Get-Command Register-Change -ErrorAction SilentlyContinue) { + Register-Change -Action StartService -Path "free-claude-code" + } + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "PM2 service registered and started" -Level INFO + } + + return $true + } finally { + Pop-Location + } + } catch { + throw "Failed to register PM2 service: $_" + } +} + +function Test-PM2Service { + <# + .SYNOPSIS + Check if the PM2 service is running + #> + [CmdletBinding()] + param() + + try { + $pm2 = Get-Command pm2 -ErrorAction SilentlyContinue + if (-not $pm2) { + return $false + } + + $output = & pm2 jlist 2>&1 | ConvertFrom-Json + + $service = $output | Where-Object { $_.name -eq 'free-claude-code' } + + return ($null -ne $service -and $service.pm2_env.status -eq 'online') + } catch { + return $false + } +} + +function Stop-PM2Service { + <# + .SYNOPSIS + Stop the PM2 service + #> + [CmdletBinding()] + param() + + try { + $pm2 = Get-Command pm2 -ErrorAction SilentlyContinue + if (-not $pm2) { + return + } + + & pm2 stop free-claude-code 2>&1 | Out-Null + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "PM2 service stopped" -Level INFO + } + } catch { + Write-Warning "Failed to stop PM2 service: $_" + } +} + +function Start-PM2Service { + <# + .SYNOPSIS + Start the PM2 service + #> + [CmdletBinding()] + param() + + try { + $pm2 = Get-Command pm2 -ErrorAction SilentlyContinue + if (-not $pm2) { + throw "PM2 not found" + } + + & pm2 start free-claude-code 2>&1 | Out-Null + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "PM2 service started" -Level INFO + } + } catch { + throw "Failed to start PM2 service: $_" + } +} + +function Get-PM2Logs { + <# + .SYNOPSIS + Get PM2 service logs + .PARAMETER Lines + Number of lines to retrieve + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [int]$Lines = 20 + ) + + try { + $pm2 = Get-Command pm2 -ErrorAction SilentlyContinue + if (-not $pm2) { + return @() + } + + $output = & pm2 logs free-claude-code --lines $Lines --nostream 2>&1 + return $output + } catch { + return @() + } +} + +Export-ModuleMember -Function New-PM2EcosystemConfig, Register-PM2Service, Test-PM2Service, Stop-PM2Service, Start-PM2Service, Get-PM2Logs diff --git a/setup/modules/PrerequisiteChecker.psm1 b/setup/modules/PrerequisiteChecker.psm1 new file mode 100644 index 00000000..672deb11 --- /dev/null +++ b/setup/modules/PrerequisiteChecker.psm1 @@ -0,0 +1,539 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Prerequisite detection and installation +.DESCRIPTION + Detects, installs, and configures all system prerequisites for the setup wizard +#> + +function Test-Prerequisites { + <# + .SYNOPSIS + Test all prerequisites and return status + #> + [CmdletBinding()] + param() + + $results = @{ + Python = Test-PythonInstalled + NodeJS = Test-NodeJSInstalled + UV = Test-UVInstalled + PM2 = Test-PM2Installed + FZF = Test-FZFInstalled + ClaudeCLI = Test-ClaudeCLIInstalled + AllRequiredPass = $false + } + + # Check if all required prerequisites pass + $results.AllRequiredPass = $results.Python.Installed -and + $results.NodeJS.Installed -and + $results.UV.Installed -and + $results.PM2.Installed -and + $results.FZF.Installed + + return $results +} + +function Test-PythonInstalled { + <# + .SYNOPSIS + Check if Python 3.14+ is installed + #> + [CmdletBinding()] + param() + + $result = @{ + Installed = $false + Version = $null + Path = $null + Status = 'Not Found' + Required = $true + } + + try { + # Try python command + $pythonCmd = Get-Command python -ErrorAction SilentlyContinue + + if ($pythonCmd) { + $versionOutput = & python --version 2>&1 + if ($versionOutput -match 'Python (\d+\.\d+\.\d+)') { + $version = [version]$matches[1] + $result.Version = $version.ToString() + $result.Path = $pythonCmd.Source + + # Check if version is 3.14+ + if ($version.Major -ge 3 -and $version.Minor -ge 14) { + $result.Installed = $true + $result.Status = 'OK' + } else { + $result.Status = "Version too old (need 3.14+, found $($version.ToString()))" + } + } + } + } catch { + $result.Status = "Error checking Python: $_" + } + + return $result +} + +function Test-NodeJSInstalled { + <# + .SYNOPSIS + Check if Node.js is installed + #> + [CmdletBinding()] + param() + + $result = @{ + Installed = $false + Version = $null + Path = $null + Status = 'Not Found' + Required = $true + } + + try { + $nodeCmd = Get-Command node -ErrorAction SilentlyContinue + + if ($nodeCmd) { + $versionOutput = & node --version 2>&1 + if ($versionOutput -match 'v(\d+\.\d+\.\d+)') { + $result.Version = $matches[1] + $result.Path = $nodeCmd.Source + $result.Installed = $true + $result.Status = 'OK' + } + } + } catch { + $result.Status = "Error checking Node.js: $_" + } + + return $result +} + +function Test-UVInstalled { + <# + .SYNOPSIS + Check if uv is installed + #> + [CmdletBinding()] + param() + + $result = @{ + Installed = $false + Version = $null + Path = $null + Status = 'Not Found' + Required = $true + } + + try { + $uvCmd = Get-Command uv -ErrorAction SilentlyContinue + + if ($uvCmd) { + $versionOutput = & uv --version 2>&1 + if ($versionOutput -match '(\d+\.\d+\.\d+)') { + $result.Version = $matches[1] + $result.Path = $uvCmd.Source + $result.Installed = $true + $result.Status = 'OK' + } + } + } catch { + $result.Status = "Error checking uv: $_" + } + + return $result +} + +function Test-PM2Installed { + <# + .SYNOPSIS + Check if PM2 is installed + #> + [CmdletBinding()] + param() + + $result = @{ + Installed = $false + Version = $null + Path = $null + Status = 'Not Found' + Required = $true + } + + try { + $pm2Cmd = Get-Command pm2 -ErrorAction SilentlyContinue + + if ($pm2Cmd) { + $versionOutput = & pm2 --version 2>&1 + if ($versionOutput -match '(\d+\.\d+\.\d+)') { + $result.Version = $matches[1] + $result.Path = $pm2Cmd.Source + $result.Installed = $true + $result.Status = 'OK' + } + } + } catch { + $result.Status = "Error checking PM2: $_" + } + + return $result +} + +function Test-FZFInstalled { + <# + .SYNOPSIS + Check if fzf is installed + #> + [CmdletBinding()] + param() + + $result = @{ + Installed = $false + Version = $null + Path = $null + Status = 'Not Found' + Required = $true + } + + try { + $fzfCmd = Get-Command fzf -ErrorAction SilentlyContinue + + if ($fzfCmd) { + $versionOutput = & fzf --version 2>&1 + if ($versionOutput -match '(\d+\.\d+)') { + $result.Version = $matches[1] + $result.Path = $fzfCmd.Source + $result.Installed = $true + $result.Status = 'OK' + } + } + } catch { + $result.Status = "Error checking fzf: $_" + } + + return $result +} + +function Test-ClaudeCLIInstalled { + <# + .SYNOPSIS + Check if Claude Code CLI is installed + #> + [CmdletBinding()] + param() + + $result = @{ + Installed = $false + Version = $null + Path = $null + Status = 'Not Found' + Required = $false + } + + try { + $claudeCmd = Get-Command claude -ErrorAction SilentlyContinue + + if ($claudeCmd) { + $result.Path = $claudeCmd.Source + $result.Installed = $true + $result.Status = 'OK' + } + } catch { + $result.Status = "Not installed (optional)" + } + + return $result +} + +function Invoke-PrerequisiteInstallation { + <# + .SYNOPSIS + Install missing prerequisites + .PARAMETER Results + Results from Test-Prerequisites + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Results + ) + + Write-Host "" + Write-Host "Installing missing prerequisites..." -ForegroundColor Cyan + Write-Host "" + + # Install Python if needed + if (-not $Results.Python.Installed) { + Write-Host "[⟳] Installing Python 3.14..." -ForegroundColor Cyan + try { + Install-Python + Write-Host "[✓] Python installed successfully" -ForegroundColor Green + } catch { + Write-Host "[✗] Failed to install Python: $_" -ForegroundColor Red + throw + } + } + + # Install Node.js if needed + if (-not $Results.NodeJS.Installed) { + Write-Host "[⟳] Installing Node.js..." -ForegroundColor Cyan + try { + Install-NodeJS + Write-Host "[✓] Node.js installed successfully" -ForegroundColor Green + } catch { + Write-Host "[✗] Failed to install Node.js: $_" -ForegroundColor Red + throw + } + } + + # Install uv if needed + if (-not $Results.UV.Installed) { + Write-Host "[⟳] Installing uv..." -ForegroundColor Cyan + try { + Install-UV + Write-Host "[✓] uv installed successfully" -ForegroundColor Green + } catch { + Write-Host "[✗] Failed to install uv: $_" -ForegroundColor Red + throw + } + } + + # Install PM2 if needed + if (-not $Results.PM2.Installed) { + Write-Host "[⟳] Installing PM2..." -ForegroundColor Cyan + try { + Install-PM2 + Write-Host "[✓] PM2 installed successfully" -ForegroundColor Green + } catch { + Write-Host "[✗] Failed to install PM2: $_" -ForegroundColor Red + throw + } + } + + # Install fzf if needed + if (-not $Results.FZF.Installed) { + Write-Host "[⟳] Installing fzf..." -ForegroundColor Cyan + try { + Install-FZF + Write-Host "[✓] fzf installed successfully" -ForegroundColor Green + } catch { + Write-Host "[✗] Failed to install fzf: $_" -ForegroundColor Red + throw + } + } + + Write-Host "" + Write-Host "All prerequisites installed!" -ForegroundColor Green + Write-Host "" +} + +function Install-Python { + <# + .SYNOPSIS + Install Python 3.14 + #> + [CmdletBinding()] + param() + + Write-Host " Downloading Python installer..." -ForegroundColor Gray + + $version = "3.14.2" + $url = "https://www.python.org/ftp/python/$version/python-$version-amd64.exe" + $installer = Join-Path $env:TEMP "python-installer.exe" + + try { + Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing -TimeoutSec 300 + + Write-Host " Installing Python (this may take a few minutes)..." -ForegroundColor Gray + + $installerArguments = "/quiet InstallAllUsers=0 PrependPath=1 Include_test=0 Include_pip=1" + $process = Start-Process -FilePath $installer -ArgumentList $installerArguments -Wait -PassThru -NoNewWindow + + if ($process.ExitCode -ne 0) { + throw "Python installer failed with exit code $($process.ExitCode)" + } + + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'User') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + + # Verify + Start-Sleep -Seconds 2 + $pythonTest = Get-Command python -ErrorAction SilentlyContinue + if (-not $pythonTest) { + throw "Python installation verification failed" + } + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "Python installed successfully" -Level INFO + } + } finally { + if (Test-Path $installer) { + Remove-Item $installer -Force -ErrorAction SilentlyContinue + } + } +} + +function Install-NodeJS { + <# + .SYNOPSIS + Install Node.js LTS + #> + [CmdletBinding()] + param() + + Write-Host " Downloading Node.js installer..." -ForegroundColor Gray + + $url = "https://nodejs.org/dist/v20.11.0/node-v20.11.0-x64.msi" + $installer = Join-Path $env:TEMP "node-installer.msi" + + try { + Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing -TimeoutSec 300 + + Write-Host " Installing Node.js (this may take a few minutes)..." -ForegroundColor Gray + + $msiArguments = "/i `"$installer`" /quiet /norestart" + $process = Start-Process -FilePath "msiexec.exe" -ArgumentList $msiArguments -Wait -PassThru -NoNewWindow + + if ($process.ExitCode -notin @(0, 3010)) { + throw "Node.js installer failed with exit code $($process.ExitCode)" + } + + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'User') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + + # Verify + Start-Sleep -Seconds 2 + $nodeTest = Get-Command node -ErrorAction SilentlyContinue + if (-not $nodeTest) { + throw "Node.js installation verification failed" + } + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "Node.js installed successfully" -Level INFO + } + } finally { + if (Test-Path $installer) { + Remove-Item $installer -Force -ErrorAction SilentlyContinue + } + } +} + +function Install-UV { + <# + .SYNOPSIS + Install uv package manager + #> + [CmdletBinding()] + param() + + Write-Host " Installing uv via pip..." -ForegroundColor Gray + + try { + $output = & python -m pip install --upgrade uv 2>&1 + + if ($LASTEXITCODE -ne 0) { + throw "pip install uv failed: $output" + } + + # Verify + Start-Sleep -Seconds 1 + $uvTest = Get-Command uv -ErrorAction SilentlyContinue + if (-not $uvTest) { + throw "uv installation verification failed" + } + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "uv installed successfully" -Level INFO + } + } catch { + throw "Failed to install uv: $_" + } +} + +function Install-PM2 { + <# + .SYNOPSIS + Install PM2 globally via npm + #> + [CmdletBinding()] + param() + + Write-Host " Installing PM2 via npm..." -ForegroundColor Gray + + try { + $output = & npm install -g pm2 2>&1 + + if ($LASTEXITCODE -ne 0) { + throw "npm install pm2 failed: $output" + } + + # Verify + Start-Sleep -Seconds 1 + $pm2Test = Get-Command pm2 -ErrorAction SilentlyContinue + if (-not $pm2Test) { + throw "PM2 installation verification failed" + } + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "PM2 installed successfully" -Level INFO + } + } catch { + throw "Failed to install PM2: $_" + } +} + +function Install-FZF { + <# + .SYNOPSIS + Install fzf + #> + [CmdletBinding()] + param() + + Write-Host " Downloading fzf..." -ForegroundColor Gray + + $url = "https://github.com/junegunn/fzf/releases/download/0.46.1/fzf-0.46.1-windows_amd64.zip" + $zipFile = Join-Path $env:TEMP "fzf.zip" + $destDir = Join-Path $env:LOCALAPPDATA "fzf" + + try { + Invoke-WebRequest -Uri $url -OutFile $zipFile -UseBasicParsing -TimeoutSec 120 + + Write-Host " Extracting fzf..." -ForegroundColor Gray + + if (Test-Path $destDir) { + Remove-Item $destDir -Recurse -Force + } + New-Item -ItemType Directory -Path $destDir -Force | Out-Null + + Expand-Archive -Path $zipFile -DestinationPath $destDir -Force + + # Add to PATH + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if ($userPath -notlike "*$destDir*") { + [Environment]::SetEnvironmentVariable('Path', "$destDir;$userPath", 'User') + $env:Path = "$destDir;$env:Path" + } + + # Verify + Start-Sleep -Seconds 1 + $fzfTest = Get-Command fzf -ErrorAction SilentlyContinue + if (-not $fzfTest) { + throw "fzf installation verification failed" + } + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "fzf installed successfully" -Level INFO + } + } finally { + if (Test-Path $zipFile) { + Remove-Item $zipFile -Force -ErrorAction SilentlyContinue + } + } +} + +Export-ModuleMember -Function Test-Prerequisites, Invoke-PrerequisiteInstallation, Test-PythonInstalled, Test-NodeJSInstalled, Test-UVInstalled, Test-PM2Installed, Test-FZFInstalled, Test-ClaudeCLIInstalled diff --git a/setup/modules/ProgressDisplay.psm1 b/setup/modules/ProgressDisplay.psm1 new file mode 100644 index 00000000..ee3d182b --- /dev/null +++ b/setup/modules/ProgressDisplay.psm1 @@ -0,0 +1,247 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Retro terminal UI components with ASCII art and neon colors +.DESCRIPTION + Provides beautiful retro/tech aesthetic with progress bars, boxes, and status indicators +#> + +function Show-RetroLogo { + <# + .SYNOPSIS + Display the ASCII art logo + #> + [CmdletBinding()] + param() + + $logoPath = Join-Path $PSScriptRoot "..\assets\logo.txt" + + if (Test-Path $logoPath) { + $logo = Get-Content $logoPath -Raw + Write-Host $logo -ForegroundColor Green + } else { + Write-Host "`n FREE CLAUDE CODE - SETUP WIZARD`n" -ForegroundColor Green + } +} + +function Show-StepIndicator { + <# + .SYNOPSIS + Display progress indicator for current step + .PARAMETER Current + Current step number + .PARAMETER Total + Total number of steps + .PARAMETER StepName + Name of the current step + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [int]$Current, + + [Parameter(Mandatory = $true)] + [int]$Total, + + [Parameter(Mandatory = $true)] + [string]$StepName + ) + + $percentage = [Math]::Round(($Current / $Total) * 100) + $barLength = 40 + $filledLength = [Math]::Round(($Current / $Total) * $barLength) + $emptyLength = $barLength - $filledLength + + $bar = ("[" + ("█" * $filledLength) + ("░" * $emptyLength) + "]") + + Write-Host "" + Write-Host "Progress: " -NoNewline -ForegroundColor Gray + Write-Host $bar -NoNewline -ForegroundColor Cyan + Write-Host " $percentage%" -ForegroundColor Cyan + Write-Host "Step $Current/$Total" -NoNewline -ForegroundColor Gray + Write-Host ": $StepName" -ForegroundColor White + Write-Host "" +} + +function Show-RetroBox { + <# + .SYNOPSIS + Draw a box with title and content + .PARAMETER Title + Box title + .PARAMETER Lines + Array of content lines + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Title, + + [Parameter(Mandatory = $true)] + [string[]]$Lines + ) + + $maxLength = ($Lines | Measure-Object -Property Length -Maximum).Maximum + $titleLength = $Title.Length + $boxWidth = [Math]::Max($maxLength, $titleLength) + 4 + + Write-Host "" + Write-Host ("┌─" + ("─" * $Title.Length) + "─┐") -ForegroundColor DarkGreen + Write-Host ("│ " + $Title + " │") -ForegroundColor Green + Write-Host ("├─" + ("─" * $Title.Length) + "─┤") -ForegroundColor DarkGreen + + foreach ($line in $Lines) { + $padding = " " * ($boxWidth - $line.Length - 4) + Write-Host ("│ " + $line + $padding + " │") -ForegroundColor Gray + } + + Write-Host ("└─" + ("─" * ($boxWidth - 4)) + "─┘") -ForegroundColor DarkGreen + Write-Host "" +} + +function Write-Status { + <# + .SYNOPSIS + Write a status message with icon + .PARAMETER Message + The message to display + .PARAMETER Level + Status level (Info, Success, Warning, Error, Processing) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + + [Parameter(Mandatory = $false)] + [ValidateSet('Info', 'Success', 'Warning', 'Error', 'Processing')] + [string]$Level = 'Info' + ) + + $icon = switch ($Level) { + 'Info' { '[ℹ]' } + 'Success' { '[✓]' } + 'Warning' { '[⚠]' } + 'Error' { '[✗]' } + 'Processing' { '[⟳]' } + } + + $color = switch ($Level) { + 'Info' { 'Cyan' } + 'Success' { 'Green' } + 'Warning' { 'Yellow' } + 'Error' { 'Red' } + 'Processing' { 'Cyan' } + } + + Write-Host "$icon " -NoNewline -ForegroundColor $color + Write-Host $Message -ForegroundColor Gray +} + +function Show-Menu { + <# + .SYNOPSIS + Display a retro-styled menu + .PARAMETER Title + Menu title + .PARAMETER Options + Array of menu options + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Title, + + [Parameter(Mandatory = $true)] + [string[]]$Options + ) + + $maxLength = ($Options | Measure-Object -Property Length -Maximum).Maximum + $boxWidth = [Math]::Max($maxLength + 8, $Title.Length + 4) + + Write-Host "" + Write-Host ("┌" + ("─" * ($boxWidth - 2)) + "┐") -ForegroundColor DarkGreen + + $titlePadding = " " * (($boxWidth - $Title.Length - 2) / 2) + Write-Host ("│" + $titlePadding + $Title + $titlePadding + "│") -ForegroundColor Green + + Write-Host ("├" + ("─" * ($boxWidth - 2)) + "┤") -ForegroundColor DarkGreen + + for ($i = 0; $i -lt $Options.Count; $i++) { + $number = " $($i + 1))" + $option = $Options[$i] + $padding = " " * ($boxWidth - $number.Length - $option.Length - 3) + Write-Host ("│" + $number + " " + $option + $padding + "│") -ForegroundColor Gray + } + + Write-Host ("└" + ("─" * ($boxWidth - 2)) + "┘") -ForegroundColor DarkGreen + Write-Host "" +} + +function Read-UserInput { + <# + .SYNOPSIS + Read user input with validation + .PARAMETER Prompt + Input prompt + .PARAMETER Default + Default value + .PARAMETER Validation + Validation scriptblock + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Prompt, + + [Parameter(Mandatory = $false)] + [string]$Default = "", + + [Parameter(Mandatory = $false)] + [scriptblock]$Validation = { $true } + ) + + do { + if ($Default) { + $userInput = Read-Host "$Prompt [$Default]" + if ([string]::IsNullOrWhiteSpace($userInput)) { + $userInput = $Default + } + } else { + $userInput = Read-Host $Prompt + } + + $validationResult = & $Validation $userInput + + if ($validationResult -eq $true) { + return $userInput + } else { + Write-Host "Invalid input: $validationResult" -ForegroundColor Red + } + } while ($true) +} + +function Read-SecureInput { + <# + .SYNOPSIS + Read secure input (masked) + .PARAMETER Prompt + Input prompt + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Prompt + ) + + $secureString = Read-Host $Prompt -AsSecureString + $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString) + try { + return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) + } finally { + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) + } +} + +Export-ModuleMember -Function Show-RetroLogo, Show-StepIndicator, Show-RetroBox, Write-Status, Show-Menu, Read-UserInput, Read-SecureInput diff --git a/setup/modules/RollbackManager.psm1 b/setup/modules/RollbackManager.psm1 new file mode 100644 index 00000000..c9fc2c19 --- /dev/null +++ b/setup/modules/RollbackManager.psm1 @@ -0,0 +1,146 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Rollback manager for tracking and reversing changes +.DESCRIPTION + Tracks all changes made during setup and provides rollback capability +#> + +$script:RollbackStack = @() + +function Register-Change { + <# + .SYNOPSIS + Register a change for potential rollback + .PARAMETER Action + Type of action performed + .PARAMETER Path + Path affected by the action + .PARAMETER BackupPath + Optional backup path for restoration + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateSet('CreateFile', 'CreateDir', 'CreateLink', 'ModifyFile', 'StartService')] + [string]$Action, + + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $false)] + [string]$BackupPath = $null + ) + + $change = [PSCustomObject]@{ + Action = $Action + Path = $Path + BackupPath = $BackupPath + Timestamp = Get-Date + } + + $script:RollbackStack += $change + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log "Registered change: $Action - $Path" -Level DEBUG + } +} + +function Invoke-Rollback { + <# + .SYNOPSIS + Execute rollback of all registered changes + #> + [CmdletBinding()] + param() + + if ($script:RollbackStack.Count -eq 0) { + Write-Host "No changes to rollback" -ForegroundColor Yellow + return + } + + Write-Host "`nRolling back changes..." -ForegroundColor Yellow + + # Process in reverse order + $changes = $script:RollbackStack | Sort-Object Timestamp -Descending + + foreach ($change in $changes) { + try { + switch ($change.Action) { + 'CreateFile' { + if (Test-Path $change.Path) { + Remove-Item -Path $change.Path -Force -ErrorAction Stop + Write-Host " [✓] Removed file: $($change.Path)" -ForegroundColor Gray + } + + if ($change.BackupPath -and (Test-Path $change.BackupPath)) { + Move-Item -Path $change.BackupPath -Destination $change.Path -Force -ErrorAction Stop + Write-Host " [✓] Restored backup: $($change.Path)" -ForegroundColor Gray + } + } + + 'CreateDir' { + if (Test-Path $change.Path) { + Remove-Item -Path $change.Path -Recurse -Force -ErrorAction Stop + Write-Host " [✓] Removed directory: $($change.Path)" -ForegroundColor Gray + } + } + + 'CreateLink' { + if (Test-Path $change.Path) { + Remove-Item -Path $change.Path -Force -ErrorAction Stop + Write-Host " [✓] Removed shortcut: $($change.Path)" -ForegroundColor Gray + } + } + + 'ModifyFile' { + if ($change.BackupPath -and (Test-Path $change.BackupPath)) { + Copy-Item -Path $change.BackupPath -Destination $change.Path -Force -ErrorAction Stop + Write-Host " [✓] Restored file: $($change.Path)" -ForegroundColor Gray + } + } + + 'StartService' { + # For PM2 services + if (Get-Command pm2 -ErrorAction SilentlyContinue) { + & pm2 stop $change.Path 2>&1 | Out-Null + & pm2 delete $change.Path 2>&1 | Out-Null + Write-Host " [✓] Stopped service: $($change.Path)" -ForegroundColor Gray + } + } + } + } catch { + Write-Host " [✗] Failed to rollback $($change.Action): $($change.Path) - $_" -ForegroundColor Red + } + } + + # Clear the stack + $script:RollbackStack = @() + + Write-Host "Rollback completed`n" -ForegroundColor Yellow +} + +function Clear-RollbackStack { + <# + .SYNOPSIS + Clear the rollback stack (after successful completion) + #> + [CmdletBinding()] + param() + + $script:RollbackStack = @() +} + +function Get-RollbackStack { + <# + .SYNOPSIS + Get the current rollback stack + #> + [CmdletBinding()] + param() + + return $script:RollbackStack +} + +Export-ModuleMember -Function Register-Change, Invoke-Rollback, Clear-RollbackStack, Get-RollbackStack diff --git a/setup/modules/ShortcutManager.psm1 b/setup/modules/ShortcutManager.psm1 new file mode 100644 index 00000000..1efa35c8 --- /dev/null +++ b/setup/modules/ShortcutManager.psm1 @@ -0,0 +1,198 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Shortcut manager for creating desktop and startup shortcuts +.DESCRIPTION + Creates Windows shortcuts for easy access to the proxy server +#> + +function New-DesktopShortcut { + <# + .SYNOPSIS + Create a desktop shortcut + .PARAMETER Target + Target executable or script + .PARAMETER ShortcutName + Name of the shortcut (without .lnk) + .PARAMETER WorkingDirectory + Working directory for the shortcut + .PARAMETER Description + Shortcut description + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Target, + + [Parameter(Mandatory = $true)] + [string]$ShortcutName, + + [Parameter(Mandatory = $false)] + [string]$WorkingDirectory = "", + + [Parameter(Mandatory = $false)] + [string]$Description = "Free Claude Code Proxy Server" + ) + + try { + $WshShell = New-Object -ComObject WScript.Shell + $Desktop = [Environment]::GetFolderPath('Desktop') + $ShortcutPath = Join-Path $Desktop "$ShortcutName.lnk" + + $Shortcut = $WshShell.CreateShortcut($ShortcutPath) + $Shortcut.TargetPath = $Target + + if ($WorkingDirectory) { + $Shortcut.WorkingDirectory = $WorkingDirectory + } else { + $Shortcut.WorkingDirectory = Split-Path $Target -Parent + } + + $Shortcut.Description = $Description + $Shortcut.IconLocation = "C:\Windows\System32\shell32.dll,13" + $Shortcut.Save() + + if (Get-Command Register-Change -ErrorAction SilentlyContinue) { + Register-Change -Action CreateLink -Path $ShortcutPath + } + + return $ShortcutPath + } catch { + throw "Failed to create desktop shortcut: $_" + } +} + +function New-StartupShortcut { + <# + .SYNOPSIS + Create a startup shortcut (auto-start on login) + .PARAMETER Target + Target executable or script + .PARAMETER ShortcutName + Name of the shortcut (without .lnk) + .PARAMETER WorkingDirectory + Working directory for the shortcut + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Target, + + [Parameter(Mandatory = $true)] + [string]$ShortcutName, + + [Parameter(Mandatory = $false)] + [string]$WorkingDirectory = "" + ) + + try { + $WshShell = New-Object -ComObject WScript.Shell + $Startup = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup" + $ShortcutPath = Join-Path $Startup "$ShortcutName.lnk" + + $Shortcut = $WshShell.CreateShortcut($ShortcutPath) + $Shortcut.TargetPath = $Target + + if ($WorkingDirectory) { + $Shortcut.WorkingDirectory = $WorkingDirectory + } else { + $Shortcut.WorkingDirectory = Split-Path $Target -Parent + } + + $Shortcut.Description = "Auto-start Free Claude Code Proxy" + $Shortcut.IconLocation = "C:\Windows\System32\shell32.dll,13" + $Shortcut.WindowStyle = 7 # Minimized + $Shortcut.Save() + + if (Get-Command Register-Change -ErrorAction SilentlyContinue) { + Register-Change -Action CreateLink -Path $ShortcutPath + } + + return $ShortcutPath + } catch { + throw "Failed to create startup shortcut: $_" + } +} + +function New-StartMenuFolder { + <# + .SYNOPSIS + Create a Start Menu folder with shortcuts + .PARAMETER FolderName + Name of the folder to create + .PARAMETER Shortcuts + Hashtable of shortcuts to create (Name = Target) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$FolderName, + + [Parameter(Mandatory = $false)] + [hashtable]$Shortcuts = @{} + ) + + try { + $StartMenu = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs" + $FolderPath = Join-Path $StartMenu $FolderName + + if (-not (Test-Path $FolderPath)) { + New-Item -ItemType Directory -Path $FolderPath -Force | Out-Null + + if (Get-Command Register-Change -ErrorAction SilentlyContinue) { + Register-Change -Action CreateDir -Path $FolderPath + } + } + + $WshShell = New-Object -ComObject WScript.Shell + + foreach ($name in $Shortcuts.Keys) { + $target = $Shortcuts[$name] + $shortcutPath = Join-Path $FolderPath "$name.lnk" + + $Shortcut = $WshShell.CreateShortcut($shortcutPath) + $Shortcut.TargetPath = $target + $Shortcut.WorkingDirectory = Split-Path $target -Parent + $Shortcut.Save() + + if (Get-Command Register-Change -ErrorAction SilentlyContinue) { + Register-Change -Action CreateLink -Path $shortcutPath + } + } + + return $FolderPath + } catch { + throw "Failed to create Start Menu folder: $_" + } +} + +function Test-ShortcutExists { + <# + .SYNOPSIS + Check if a shortcut exists + .PARAMETER ShortcutName + Name of the shortcut to check + .PARAMETER Location + Location to check (Desktop, Startup, StartMenu) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ShortcutName, + + [Parameter(Mandatory = $true)] + [ValidateSet('Desktop', 'Startup', 'StartMenu')] + [string]$Location + ) + + $path = switch ($Location) { + 'Desktop' { Join-Path ([Environment]::GetFolderPath('Desktop')) "$ShortcutName.lnk" } + 'Startup' { Join-Path "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup" "$ShortcutName.lnk" } + 'StartMenu' { Join-Path "$env:APPDATA\Microsoft\Windows\Start Menu\Programs" "$ShortcutName.lnk" } + } + + return Test-Path $path +} + +Export-ModuleMember -Function New-DesktopShortcut, New-StartupShortcut, New-StartMenuFolder, Test-ShortcutExists diff --git a/setup/modules/UIModules.psm1 b/setup/modules/UIModules.psm1 new file mode 100644 index 00000000..35bb1c99 --- /dev/null +++ b/setup/modules/UIModules.psm1 @@ -0,0 +1,207 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Reusable UI components for the setup wizard +.DESCRIPTION + Provides common UI functions like confirmations, choices, and headers +#> + +function Show-Header { + <# + .SYNOPSIS + Display a section header + .PARAMETER Title + The title to display + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Title + ) + + Write-Host "" + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host " $Title" -ForegroundColor Cyan + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host "" +} + +function Read-Confirmation { + <# + .SYNOPSIS + Ask user for yes/no confirmation + .PARAMETER Prompt + The question to ask + .PARAMETER DefaultYes + Whether to default to Yes + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Prompt, + + [Parameter(Mandatory = $false)] + [switch]$DefaultYes + ) + + $choices = if ($DefaultYes) { 'Y/n' } else { 'y/N' } + + do { + $response = Read-Host "$Prompt [$choices]" + + if ([string]::IsNullOrWhiteSpace($response)) { + return $DefaultYes.IsPresent + } + + $response = $response.Trim().ToLower() + + if ($response -eq 'y' -or $response -eq 'yes') { + return $true + } elseif ($response -eq 'n' -or $response -eq 'no') { + return $false + } + + Write-Host "Please enter 'y' or 'n'" -ForegroundColor Yellow + } while ($true) +} + +function Read-Choice { + <# + .SYNOPSIS + Display a numbered menu and get user choice + .PARAMETER Prompt + The prompt to display + .PARAMETER Options + Array of options to choose from + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Prompt, + + [Parameter(Mandatory = $true)] + [string[]]$Options + ) + + Write-Host $Prompt -ForegroundColor Cyan + Write-Host "" + + for ($i = 0; $i -lt $Options.Count; $i++) { + Write-Host " $($i + 1)) $($Options[$i])" -ForegroundColor Gray + } + + Write-Host "" + + do { + $response = Read-Host "Choice [1-$($Options.Count)]" + + if ($response -match '^\d+$') { + $choice = [int]$response + if ($choice -ge 1 -and $choice -le $Options.Count) { + return $choice - 1 + } + } + + Write-Host "Please enter a number between 1 and $($Options.Count)" -ForegroundColor Yellow + } while ($true) +} + +function Show-MessageBox { + <# + .SYNOPSIS + Display a message in a box + .PARAMETER Message + The message to display + .PARAMETER Type + Message type (Info, Warning, Error) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + + [Parameter(Mandatory = $false)] + [ValidateSet('Info', 'Warning', 'Error')] + [string]$Type = 'Info' + ) + + $color = switch ($Type) { + 'Info' { 'Cyan' } + 'Warning' { 'Yellow' } + 'Error' { 'Red' } + } + + $icon = switch ($Type) { + 'Info' { 'ℹ' } + 'Warning' { '⚠' } + 'Error' { '✗' } + } + + $lines = $Message -split "`n" + $maxLength = ($lines | Measure-Object -Property Length -Maximum).Maximum + $boxWidth = [Math]::Min([Math]::Max($maxLength + 4, 40), 70) + + Write-Host "" + Write-Host ("┌" + ("─" * ($boxWidth - 2)) + "┐") -ForegroundColor $color + + foreach ($line in $lines) { + $padding = " " * ($boxWidth - $line.Length - 4) + Write-Host ("│ $icon $line$padding │") -ForegroundColor $color + } + + Write-Host ("└" + ("─" * ($boxWidth - 2)) + "┘") -ForegroundColor $color + Write-Host "" +} + +function Test-IsAdmin { + <# + .SYNOPSIS + Check if running with administrator privileges + #> + [CmdletBinding()] + param() + + $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Show-ProgressSpinner { + <# + .SYNOPSIS + Display a spinner while executing an action + .PARAMETER Message + Message to display + .PARAMETER Action + ScriptBlock to execute + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + + [Parameter(Mandatory = $true)] + [scriptblock]$Action + ) + + $spinner = @('|', '/', '-', '\') + $i = 0 + + $job = Start-Job -ScriptBlock $Action + + while ($job.State -eq 'Running') { + Write-Host "`r$Message $($spinner[$i % $spinner.Length])" -NoNewline -ForegroundColor Cyan + $i++ + Start-Sleep -Milliseconds 100 + } + + $result = Receive-Job $job + Remove-Job $job + + Write-Host "`r$Message ✓" -ForegroundColor Green + + return $result +} + +Export-ModuleMember -Function Show-Header, Read-Confirmation, Read-Choice, Show-MessageBox, Test-IsAdmin, Show-ProgressSpinner diff --git a/setup/modules/Validator.psm1 b/setup/modules/Validator.psm1 new file mode 100644 index 00000000..85e05e9f --- /dev/null +++ b/setup/modules/Validator.psm1 @@ -0,0 +1,254 @@ +#requires -Version 5.1 + +<# +.SYNOPSIS + Post-installation validator +.DESCRIPTION + Verifies that the installation was successful and all components are working +#> + +function Test-Installation { + <# + .SYNOPSIS + Run comprehensive installation tests + .PARAMETER ProjectRoot + Root directory of the project + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ProjectRoot + ) + + $results = [PSCustomObject]@{ + PythonEnvironment = $false + Dependencies = $false + Configuration = $false + PM2Service = $false + ServerStartup = $false + APIHealth = $false + ModelsEndpoint = $false + Warnings = @() + Errors = @() + AllPass = $false + } + + # Test 1: Python Environment + try { + $venvPath = Join-Path $ProjectRoot ".venv" + if (Test-Path $venvPath) { + $results.PythonEnvironment = $true + } else { + $results.Errors += "Virtual environment not found at: $venvPath" + } + } catch { + $results.Errors += "Python environment check failed: $_" + } + + # Test 2: Dependencies + try { + $pythonExe = Join-Path $ProjectRoot ".venv\Scripts\python.exe" + if (Test-Path $pythonExe) { + $testImport = & $pythonExe -c "import fastapi, uvicorn, httpx; print('OK')" 2>&1 + if ($testImport -match 'OK') { + $results.Dependencies = $true + } else { + $results.Errors += "Failed to import required Python packages" + } + } else { + $results.Errors += "Python executable not found in virtual environment" + } + } catch { + $results.Errors += "Dependency check failed: $_" + } + + # Test 3: Configuration + try { + $envPath = Join-Path $ProjectRoot ".env" + if (Test-Path $envPath) { + $envContent = Get-Content $envPath -Raw + + # Check for required keys + $requiredKeys = @('PROVIDER_TYPE', 'MODEL') + $missingKeys = @() + + foreach ($key in $requiredKeys) { + if ($envContent -notmatch "$key=") { + $missingKeys += $key + } + } + + if ($missingKeys.Count -eq 0) { + $results.Configuration = $true + } else { + $results.Errors += "Missing required configuration keys: $($missingKeys -join ', ')" + } + } else { + $results.Errors += ".env file not found" + } + } catch { + $results.Errors += "Configuration check failed: $_" + } + + # Test 4: PM2 Service + try { + if (Get-Command pm2 -ErrorAction SilentlyContinue) { + $pm2List = & pm2 jlist 2>&1 | ConvertFrom-Json + $service = $pm2List | Where-Object { $_.name -eq 'free-claude-code' } + + if ($service) { + if ($service.pm2_env.status -eq 'online') { + $results.PM2Service = $true + } else { + $results.Warnings += "PM2 service exists but is not online (status: $($service.pm2_env.status))" + } + } else { + $results.Warnings += "PM2 service not registered" + } + } else { + $results.Warnings += "PM2 not found" + } + } catch { + $results.Warnings += "PM2 service check failed: $_" + } + + # Test 5: Server Startup (if PM2 is running) + if ($results.PM2Service) { + try { + Start-Sleep -Seconds 2 # Give server time to start + + $response = Invoke-WebRequest -Uri "http://localhost:8082/health" -Method GET -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop + + if ($response.StatusCode -eq 200) { + $results.ServerStartup = $true + $results.APIHealth = $true + } + } catch { + $results.Warnings += "Server health check failed: $_" + } + + # Test 6: Models Endpoint + try { + $response = Invoke-WebRequest -Uri "http://localhost:8082/v1/models" -Method GET -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop + + if ($response.StatusCode -eq 200) { + $results.ModelsEndpoint = $true + } + } catch { + $results.Warnings += "Models endpoint check failed: $_" + } + } + + # Test 7: Claude Code CLI (warning only) + try { + $claudeCmd = Get-Command claude -ErrorAction SilentlyContinue + if (-not $claudeCmd) { + $results.Warnings += "Claude Code CLI not found in PATH. Install from: https://github.com/anthropics/claude-code" + } + } catch { + # Ignore + } + + # Calculate overall pass/fail + $results.AllPass = $results.PythonEnvironment -and + $results.Dependencies -and + $results.Configuration -and + ($results.APIHealth -or $results.PM2Service) + + return $results +} + +function Show-VerificationReport { + <# + .SYNOPSIS + Display verification results in a nice format + .PARAMETER Results + Results object from Test-Installation + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [PSCustomObject]$Results + ) + + Write-Host "" + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host " Verification Report" -ForegroundColor Cyan + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host "" + + # Display test results + $tests = @( + @{ Name = "Python Environment"; Pass = $Results.PythonEnvironment }, + @{ Name = "Dependencies"; Pass = $Results.Dependencies }, + @{ Name = "Configuration"; Pass = $Results.Configuration }, + @{ Name = "PM2 Service"; Pass = $Results.PM2Service }, + @{ Name = "Server Startup"; Pass = $Results.ServerStartup }, + @{ Name = "API Health Check"; Pass = $Results.APIHealth }, + @{ Name = "Models Endpoint"; Pass = $Results.ModelsEndpoint } + ) + + foreach ($test in $tests) { + $icon = if ($test.Pass) { "[✓]" } else { "[✗]" } + $color = if ($test.Pass) { "Green" } else { "Red" } + + Write-Host "$icon " -NoNewline -ForegroundColor $color + Write-Host $test.Name -ForegroundColor Gray + } + + # Display warnings + if ($Results.Warnings.Count -gt 0) { + Write-Host "" + Write-Host "Warnings:" -ForegroundColor Yellow + foreach ($warning in $Results.Warnings) { + Write-Host " [⚠] $warning" -ForegroundColor Yellow + } + } + + # Display errors + if ($Results.Errors.Count -gt 0) { + Write-Host "" + Write-Host "Errors:" -ForegroundColor Red + foreach ($err in $Results.Errors) { + Write-Host " [✗] $err" -ForegroundColor Red + } + } + + Write-Host "" + + if ($Results.AllPass) { + Write-Host "✓ All critical tests passed!" -ForegroundColor Green + } else { + Write-Host "✗ Some tests failed. Please review the errors above." -ForegroundColor Red + } + + Write-Host "" +} + +function Invoke-HealthCheck { + <# + .SYNOPSIS + Perform a simple health check on the server + .PARAMETER Url + Base URL of the server + .PARAMETER Timeout + Timeout in seconds + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$Url = "http://localhost:8082", + + [Parameter(Mandatory = $false)] + [int]$Timeout = 5 + ) + + try { + $response = Invoke-RestMethod -Uri "$Url/health" -Method GET -TimeoutSec $Timeout + return $response.status -eq 'ok' + } catch { + return $false + } +} + +Export-ModuleMember -Function Test-Installation, Show-VerificationReport, Invoke-HealthCheck diff --git a/start-claude-proxy.bat b/start-claude-proxy.bat new file mode 100644 index 00000000..5c01a2ab --- /dev/null +++ b/start-claude-proxy.bat @@ -0,0 +1,20 @@ +@echo off +REM Auto-start Claude Proxy Server +echo Starting Claude Proxy Server... + +REM Add npm global packages to PATH +set PATH=%APPDATA%\npm;%PATH% + +REM Start the proxy using PM2 +pm2 resurrect + +echo. +echo Claude Proxy Status: +pm2 status + +echo. +echo Proxy server is running at http://localhost:8082 +echo. +echo You can close this window - the proxy runs in the background. +timeout /t 5 /nobreak >nul +exit diff --git a/vscode-settings-snippet.json b/vscode-settings-snippet.json new file mode 100644 index 00000000..350894a1 --- /dev/null +++ b/vscode-settings-snippet.json @@ -0,0 +1,6 @@ +{ + "claude-code.environmentVariables": [ + { "name": "ANTHROPIC_BASE_URL", "value": "http://localhost:8082" }, + { "name": "ANTHROPIC_AUTH_TOKEN", "value": "freecc" } + ] +}