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.
[](https://opensource.org/licenses/MIT)
@@ -12,10 +15,10 @@
[](https://github.com/astral-sh/ruff)
[](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" }
+ ]
+}