free-claude-code 6.2.64__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- free_claude_code/__init__.py +1 -0
- free_claude_code/api/__init__.py +1 -0
- free_claude_code/api/admin_cache.py +49 -0
- free_claude_code/api/admin_routes.py +501 -0
- free_claude_code/api/admin_security.py +67 -0
- free_claude_code/api/admin_static/admin.css +980 -0
- free_claude_code/api/admin_static/admin.js +1934 -0
- free_claude_code/api/admin_static/code_sessions.css +109 -0
- free_claude_code/api/admin_static/code_sessions.js +1693 -0
- free_claude_code/api/admin_static/form_controls.js +15 -0
- free_claude_code/api/admin_static/index.html +201 -0
- free_claude_code/api/admin_static/model_combobox.js +194 -0
- free_claude_code/api/admin_static/providers/LICENSE-llamacpp.txt +21 -0
- free_claude_code/api/admin_static/providers/LICENSE-lobe-icons.txt +21 -0
- free_claude_code/api/admin_static/providers/SOURCES.md +56 -0
- free_claude_code/api/admin_static/providers/agnesai.svg +1 -0
- free_claude_code/api/admin_static/providers/azure-color.svg +1 -0
- free_claude_code/api/admin_static/providers/bedrock-color.svg +1 -0
- free_claude_code/api/admin_static/providers/cerebras-color.svg +1 -0
- free_claude_code/api/admin_static/providers/chutes.png +0 -0
- free_claude_code/api/admin_static/providers/cline.svg +1 -0
- free_claude_code/api/admin_static/providers/cloudflare-color.svg +1 -0
- free_claude_code/api/admin_static/providers/cohere-color.svg +1 -0
- free_claude_code/api/admin_static/providers/deepinfra-color.svg +1 -0
- free_claude_code/api/admin_static/providers/deepseek-color.svg +1 -0
- free_claude_code/api/admin_static/providers/experiential.svg +9 -0
- free_claude_code/api/admin_static/providers/featherless-color.svg +1 -0
- free_claude_code/api/admin_static/providers/fireworks-color.svg +1 -0
- free_claude_code/api/admin_static/providers/gemini-color.svg +1 -0
- free_claude_code/api/admin_static/providers/githubcopilot.svg +1 -0
- free_claude_code/api/admin_static/providers/groq.svg +1 -0
- free_claude_code/api/admin_static/providers/huggingface-color.svg +1 -0
- free_claude_code/api/admin_static/providers/kilocode.svg +1 -0
- free_claude_code/api/admin_static/providers/kimi-color.svg +1 -0
- free_claude_code/api/admin_static/providers/lightning.png +0 -0
- free_claude_code/api/admin_static/providers/llamacpp.svg +77 -0
- free_claude_code/api/admin_static/providers/llm7.png +0 -0
- free_claude_code/api/admin_static/providers/lmstudio.svg +1 -0
- free_claude_code/api/admin_static/providers/minimax-color.svg +1 -0
- free_claude_code/api/admin_static/providers/mistral-color.svg +1 -0
- free_claude_code/api/admin_static/providers/nararoute.png +0 -0
- free_claude_code/api/admin_static/providers/nebius.svg +1 -0
- free_claude_code/api/admin_static/providers/novita-color.svg +1 -0
- free_claude_code/api/admin_static/providers/nvidia-color.svg +1 -0
- free_claude_code/api/admin_static/providers/ollama.svg +1 -0
- free_claude_code/api/admin_static/providers/openai.svg +1 -0
- free_claude_code/api/admin_static/providers/opencode.svg +1 -0
- free_claude_code/api/admin_static/providers/openrouter.svg +1 -0
- free_claude_code/api/admin_static/providers/poolside-color.svg +1 -0
- free_claude_code/api/admin_static/providers/qwen-color.svg +1 -0
- free_claude_code/api/admin_static/providers/sambanova-color.svg +1 -0
- free_claude_code/api/admin_static/providers/scaleway.svg +11 -0
- free_claude_code/api/admin_static/providers/siliconcloud-color.svg +1 -0
- free_claude_code/api/admin_static/providers/together-color.svg +1 -0
- free_claude_code/api/admin_static/providers/tokenrouter.png +0 -0
- free_claude_code/api/admin_static/providers/vercel.svg +1 -0
- free_claude_code/api/admin_static/providers/vertexai-color.svg +1 -0
- free_claude_code/api/admin_static/providers/wafer.png +0 -0
- free_claude_code/api/admin_static/providers/wandb.png +0 -0
- free_claude_code/api/admin_static/providers/xai.svg +1 -0
- free_claude_code/api/admin_static/providers/zai.svg +1 -0
- free_claude_code/api/admin_static/providers/zenmux.svg +1 -0
- free_claude_code/api/admin_static/session_layout.css +552 -0
- free_claude_code/api/admin_static/session_ui.js +202 -0
- free_claude_code/api/app.py +149 -0
- free_claude_code/api/code_sessions_routes.py +386 -0
- free_claude_code/api/command_utils.py +164 -0
- free_claude_code/api/dependencies.py +128 -0
- free_claude_code/api/detection.py +185 -0
- free_claude_code/api/handlers/__init__.py +7 -0
- free_claude_code/api/handlers/classifier_response.py +66 -0
- free_claude_code/api/handlers/messages.py +359 -0
- free_claude_code/api/handlers/responses.py +163 -0
- free_claude_code/api/handlers/token_count.py +87 -0
- free_claude_code/api/markdown.py +71 -0
- free_claude_code/api/model_catalog.py +282 -0
- free_claude_code/api/optimization_handlers.py +162 -0
- free_claude_code/api/ports.py +97 -0
- free_claude_code/api/request_errors.py +100 -0
- free_claude_code/api/request_ids.py +100 -0
- free_claude_code/api/request_lifetime.py +130 -0
- free_claude_code/api/response_streams.py +422 -0
- free_claude_code/api/routes.py +269 -0
- free_claude_code/api/validation_log.py +46 -0
- free_claude_code/application/__init__.py +1 -0
- free_claude_code/application/code_sessions/__init__.py +33 -0
- free_claude_code/application/code_sessions/models.py +227 -0
- free_claude_code/application/code_sessions/ports.py +197 -0
- free_claude_code/application/code_sessions/service.py +1123 -0
- free_claude_code/application/code_sessions/state.py +659 -0
- free_claude_code/application/connected_accounts.py +96 -0
- free_claude_code/application/errors.py +41 -0
- free_claude_code/application/execution.py +467 -0
- free_claude_code/application/model_catalog.py +95 -0
- free_claude_code/application/model_metadata.py +26 -0
- free_claude_code/application/ports.py +127 -0
- free_claude_code/application/readiness.py +43 -0
- free_claude_code/application/reasoning.py +134 -0
- free_claude_code/application/routing.py +291 -0
- free_claude_code/application/session_events.py +140 -0
- free_claude_code/application/web_tools/__init__.py +1 -0
- free_claude_code/application/web_tools/ports.py +35 -0
- free_claude_code/application/web_tools/request.py +348 -0
- free_claude_code/application/web_tools/service.py +481 -0
- free_claude_code/assets/app-icon.icns +0 -0
- free_claude_code/assets/app-icon.ico +0 -0
- free_claude_code/assets/app-icon.png +0 -0
- free_claude_code/assets/app-icon.svg +14 -0
- free_claude_code/cli/__init__.py +5 -0
- free_claude_code/cli/commands.py +379 -0
- free_claude_code/cli/desktop.py +156 -0
- free_claude_code/cli/desktop_assets.py +31 -0
- free_claude_code/cli/desktop_entrypoint.py +29 -0
- free_claude_code/cli/desktop_tray.py +67 -0
- free_claude_code/cli/entrypoints.py +25 -0
- free_claude_code/cli/launchers/__init__.py +1 -0
- free_claude_code/cli/launchers/aider.py +53 -0
- free_claude_code/cli/launchers/aider_config.py +80 -0
- free_claude_code/cli/launchers/catalog_http.py +134 -0
- free_claude_code/cli/launchers/claude.py +34 -0
- free_claude_code/cli/launchers/cline.py +60 -0
- free_claude_code/cli/launchers/cline_config.py +121 -0
- free_claude_code/cli/launchers/codex.py +47 -0
- free_claude_code/cli/launchers/common.py +103 -0
- free_claude_code/cli/launchers/dsh.py +69 -0
- free_claude_code/cli/launchers/dsh_config.py +124 -0
- free_claude_code/cli/launchers/grok.py +116 -0
- free_claude_code/cli/launchers/hermes.py +85 -0
- free_claude_code/cli/launchers/hermes_config.py +134 -0
- free_claude_code/cli/launchers/muse.py +68 -0
- free_claude_code/cli/launchers/opencode.py +83 -0
- free_claude_code/cli/launchers/opencode_config.py +107 -0
- free_claude_code/cli/launchers/pi.py +70 -0
- free_claude_code/cli/launchers/pi_extension.ts +190 -0
- free_claude_code/cli/launchers/runner.py +155 -0
- free_claude_code/cli/local_http.py +12 -0
- free_claude_code/cli/managed/__init__.py +6 -0
- free_claude_code/cli/managed/claude.py +225 -0
- free_claude_code/cli/managed/diagnostics.py +47 -0
- free_claude_code/cli/managed/manager.py +207 -0
- free_claude_code/cli/managed/session.py +293 -0
- free_claude_code/cli/process_registry.py +76 -0
- free_claude_code/cli/server_socket.py +71 -0
- free_claude_code/cli/uvicorn_server.py +41 -0
- free_claude_code/config/__init__.py +10 -0
- free_claude_code/config/admin/__init__.py +1 -0
- free_claude_code/config/admin/manifest.py +610 -0
- free_claude_code/config/admin/persistence.py +202 -0
- free_claude_code/config/admin/provider_manifest.py +443 -0
- free_claude_code/config/admin/specs.py +77 -0
- free_claude_code/config/admin/state.py +18 -0
- free_claude_code/config/admin/status.py +116 -0
- free_claude_code/config/admin/validation.py +40 -0
- free_claude_code/config/admin/values.py +115 -0
- free_claude_code/config/constants.py +9 -0
- free_claude_code/config/env_files.py +74 -0
- free_claude_code/config/env_migrations.py +457 -0
- free_claude_code/config/loader.py +196 -0
- free_claude_code/config/logging_config.py +197 -0
- free_claude_code/config/model_refs.py +118 -0
- free_claude_code/config/nim.py +121 -0
- free_claude_code/config/paths.py +104 -0
- free_claude_code/config/provider_catalog.py +746 -0
- free_claude_code/config/provider_proxies.py +44 -0
- free_claude_code/config/reasoning.py +24 -0
- free_claude_code/config/server_urls.py +55 -0
- free_claude_code/config/settings.py +812 -0
- free_claude_code/core/__init__.py +15 -0
- free_claude_code/core/anthropic/__init__.py +99 -0
- free_claude_code/core/anthropic/content.py +31 -0
- free_claude_code/core/anthropic/conversion.py +876 -0
- free_claude_code/core/anthropic/errors.py +97 -0
- free_claude_code/core/anthropic/image_sources.py +77 -0
- free_claude_code/core/anthropic/models.py +193 -0
- free_claude_code/core/anthropic/native.py +184 -0
- free_claude_code/core/anthropic/native_stream.py +306 -0
- free_claude_code/core/anthropic/request_serialization.py +57 -0
- free_claude_code/core/anthropic/request_snapshot.py +35 -0
- free_claude_code/core/anthropic/server_tool_sse.py +234 -0
- free_claude_code/core/anthropic/server_tool_types.py +9 -0
- free_claude_code/core/anthropic/sse_aggregation.py +144 -0
- free_claude_code/core/anthropic/stream_contracts.py +208 -0
- free_claude_code/core/anthropic/streaming/__init__.py +43 -0
- free_claude_code/core/anthropic/streaming/decoder.py +49 -0
- free_claude_code/core/anthropic/streaming/emitter.py +62 -0
- free_claude_code/core/anthropic/streaming/ledger.py +538 -0
- free_claude_code/core/anthropic/streaming/recovery.py +211 -0
- free_claude_code/core/anthropic/thinking.py +140 -0
- free_claude_code/core/anthropic/tokens.py +111 -0
- free_claude_code/core/anthropic/tool_results.py +63 -0
- free_claude_code/core/anthropic/tool_schema.py +94 -0
- free_claude_code/core/anthropic/tools.py +493 -0
- free_claude_code/core/anthropic/usage.py +38 -0
- free_claude_code/core/anthropic/utils.py +9 -0
- free_claude_code/core/async_iterators.py +26 -0
- free_claude_code/core/async_tasks.py +26 -0
- free_claude_code/core/data/README.md +12 -0
- free_claude_code/core/data/TIKTOKEN-LICENSE +21 -0
- free_claude_code/core/data/cl100k_base.tiktoken +100256 -0
- free_claude_code/core/diagnostics.py +319 -0
- free_claude_code/core/failures.py +50 -0
- free_claude_code/core/gateway_model_ids.py +73 -0
- free_claude_code/core/history_replay.py +561 -0
- free_claude_code/core/interprocess_lock.py +90 -0
- free_claude_code/core/json_types.py +7 -0
- free_claude_code/core/model_capabilities.py +10 -0
- free_claude_code/core/openai_chat.py +54 -0
- free_claude_code/core/openai_responses/__init__.py +90 -0
- free_claude_code/core/openai_responses/chat_request.py +747 -0
- free_claude_code/core/openai_responses/errors.py +78 -0
- free_claude_code/core/openai_responses/events.py +82 -0
- free_claude_code/core/openai_responses/ids.py +32 -0
- free_claude_code/core/openai_responses/items.py +35 -0
- free_claude_code/core/openai_responses/messages_request.py +624 -0
- free_claude_code/core/openai_responses/messages_stream.py +404 -0
- free_claude_code/core/openai_responses/models.py +33 -0
- free_claude_code/core/openai_responses/native.py +147 -0
- free_claude_code/core/openai_responses/provider_input.py +409 -0
- free_claude_code/core/openai_responses/provider_stream.py +282 -0
- free_claude_code/core/openai_responses/reasoning.py +73 -0
- free_claude_code/core/openai_responses/streaming/__init__.py +1 -0
- free_claude_code/core/openai_responses/streaming/blocks.py +36 -0
- free_claude_code/core/openai_responses/streaming/completion.py +169 -0
- free_claude_code/core/openai_responses/streaming/error_mapping.py +28 -0
- free_claude_code/core/openai_responses/streaming/event_builders.py +198 -0
- free_claude_code/core/openai_responses/streaming/ledger.py +96 -0
- free_claude_code/core/openai_responses/tokens.py +133 -0
- free_claude_code/core/openai_responses/tool_adaptation.py +828 -0
- free_claude_code/core/openai_responses/tool_search.py +204 -0
- free_claude_code/core/openai_responses/tools.py +169 -0
- free_claude_code/core/openai_tool_names.py +178 -0
- free_claude_code/core/rate_limit.py +72 -0
- free_claude_code/core/reasoning.py +136 -0
- free_claude_code/core/token_estimation.py +76 -0
- free_claude_code/core/tool_schema_patterns.py +168 -0
- free_claude_code/core/trace.py +197 -0
- free_claude_code/core/version.py +15 -0
- free_claude_code/core/web_tools.py +17 -0
- free_claude_code/core/windows_dpi.py +27 -0
- free_claude_code/harnesses/__init__.py +1 -0
- free_claude_code/harnesses/claude.py +42 -0
- free_claude_code/harnesses/claude_desktop_integration.py +418 -0
- free_claude_code/harnesses/claude_integration.py +167 -0
- free_claude_code/harnesses/codex.py +94 -0
- free_claude_code/harnesses/codex_integration.py +147 -0
- free_claude_code/harnesses/codex_model_catalog.py +109 -0
- free_claude_code/harnesses/config_file.py +33 -0
- free_claude_code/harnesses/environment.py +78 -0
- free_claude_code/harnesses/jetbrains_acp_integration.py +242 -0
- free_claude_code/harnesses/launch.py +21 -0
- free_claude_code/harnesses/resources.py +42 -0
- free_claude_code/messaging/__init__.py +16 -0
- free_claude_code/messaging/cli_event_constants.py +67 -0
- free_claude_code/messaging/command_context.py +99 -0
- free_claude_code/messaging/command_dispatcher.py +31 -0
- free_claude_code/messaging/commands.py +182 -0
- free_claude_code/messaging/event_parser.py +184 -0
- free_claude_code/messaging/limiter.py +342 -0
- free_claude_code/messaging/managed_protocols.py +35 -0
- free_claude_code/messaging/models.py +57 -0
- free_claude_code/messaging/node_event_pipeline.py +120 -0
- free_claude_code/messaging/node_runner.py +408 -0
- free_claude_code/messaging/platforms/__init__.py +20 -0
- free_claude_code/messaging/platforms/discord.py +319 -0
- free_claude_code/messaging/platforms/discord_inbound.py +112 -0
- free_claude_code/messaging/platforms/discord_io.py +167 -0
- free_claude_code/messaging/platforms/factory.py +109 -0
- free_claude_code/messaging/platforms/outbox.py +143 -0
- free_claude_code/messaging/platforms/ports.py +99 -0
- free_claude_code/messaging/platforms/telegram.py +300 -0
- free_claude_code/messaging/platforms/telegram_inbound.py +133 -0
- free_claude_code/messaging/platforms/telegram_io.py +287 -0
- free_claude_code/messaging/platforms/voice_flow.py +388 -0
- free_claude_code/messaging/rendering/__init__.py +3 -0
- free_claude_code/messaging/rendering/discord_markdown.py +318 -0
- free_claude_code/messaging/rendering/markdown_tables.py +47 -0
- free_claude_code/messaging/rendering/profiles.py +53 -0
- free_claude_code/messaging/rendering/telegram_markdown.py +331 -0
- free_claude_code/messaging/safe_diagnostics.py +15 -0
- free_claude_code/messaging/session/__init__.py +5 -0
- free_claude_code/messaging/session/managed_message_log.py +135 -0
- free_claude_code/messaging/session/persistence.py +161 -0
- free_claude_code/messaging/session/store.py +162 -0
- free_claude_code/messaging/transcript/__init__.py +6 -0
- free_claude_code/messaging/transcript/buffer.py +220 -0
- free_claude_code/messaging/transcript/context.py +18 -0
- free_claude_code/messaging/transcript/renderer.py +65 -0
- free_claude_code/messaging/transcript/segments.py +176 -0
- free_claude_code/messaging/transcript/subagents.py +108 -0
- free_claude_code/messaging/transcription.py +132 -0
- free_claude_code/messaging/trees/__init__.py +41 -0
- free_claude_code/messaging/trees/graph.py +250 -0
- free_claude_code/messaging/trees/identity.py +16 -0
- free_claude_code/messaging/trees/manager.py +587 -0
- free_claude_code/messaging/trees/node.py +56 -0
- free_claude_code/messaging/trees/processor.py +272 -0
- free_claude_code/messaging/trees/queue.py +47 -0
- free_claude_code/messaging/trees/repository.py +135 -0
- free_claude_code/messaging/trees/runtime.py +487 -0
- free_claude_code/messaging/trees/snapshot.py +221 -0
- free_claude_code/messaging/trees/transitions.py +174 -0
- free_claude_code/messaging/turn_intake.py +241 -0
- free_claude_code/messaging/ui_updates.py +99 -0
- free_claude_code/messaging/voice.py +376 -0
- free_claude_code/messaging/workflow.py +714 -0
- free_claude_code/providers/__init__.py +12 -0
- free_claude_code/providers/admission.py +942 -0
- free_claude_code/providers/anthropic_messages/__init__.py +1 -0
- free_claude_code/providers/anthropic_messages/request_policy.py +162 -0
- free_claude_code/providers/anthropic_messages/transport.py +499 -0
- free_claude_code/providers/base.py +73 -0
- free_claude_code/providers/cloudflare/__init__.py +8 -0
- free_claude_code/providers/cloudflare/client.py +166 -0
- free_claude_code/providers/credential_validation.py +314 -0
- free_claude_code/providers/deepseek/__init__.py +5 -0
- free_claude_code/providers/deepseek/client.py +117 -0
- free_claude_code/providers/deepseek/compat.py +241 -0
- free_claude_code/providers/endpoint.py +21 -0
- free_claude_code/providers/endpoint_types.py +21 -0
- free_claude_code/providers/failure_policy.py +574 -0
- free_claude_code/providers/gemini/__init__.py +5 -0
- free_claude_code/providers/gemini/client.py +42 -0
- free_claude_code/providers/github_copilot/__init__.py +1 -0
- free_claude_code/providers/github_copilot/auth.py +502 -0
- free_claude_code/providers/github_copilot/broker.py +248 -0
- free_claude_code/providers/github_copilot/lifecycle.py +18 -0
- free_claude_code/providers/github_copilot/login.py +112 -0
- free_claude_code/providers/github_copilot/native_cli.py +82 -0
- free_claude_code/providers/github_copilot/provider.py +367 -0
- free_claude_code/providers/github_copilot/request_policy.py +119 -0
- free_claude_code/providers/github_copilot/responses_events.py +90 -0
- free_claude_code/providers/github_copilot/sdk.py +496 -0
- free_claude_code/providers/github_copilot/types.py +67 -0
- free_claude_code/providers/google_openai/__init__.py +17 -0
- free_claude_code/providers/google_openai/provider.py +73 -0
- free_claude_code/providers/google_openai/reasoning.py +150 -0
- free_claude_code/providers/google_openai/thought_signatures.py +117 -0
- free_claude_code/providers/groq/__init__.py +5 -0
- free_claude_code/providers/groq/client.py +342 -0
- free_claude_code/providers/groq/tpm.py +114 -0
- free_claude_code/providers/history_replay.py +260 -0
- free_claude_code/providers/http.py +105 -0
- free_claude_code/providers/kilo/__init__.py +5 -0
- free_claude_code/providers/kilo/client.py +45 -0
- free_claude_code/providers/kilo/models.py +60 -0
- free_claude_code/providers/lmstudio/__init__.py +5 -0
- free_claude_code/providers/lmstudio/client.py +220 -0
- free_claude_code/providers/mistral/__init__.py +5 -0
- free_claude_code/providers/mistral/client.py +91 -0
- free_claude_code/providers/mistral/reasoning.py +410 -0
- free_claude_code/providers/model_listing.py +479 -0
- free_claude_code/providers/nvidia_nim/__init__.py +5 -0
- free_claude_code/providers/nvidia_nim/client.py +224 -0
- free_claude_code/providers/nvidia_nim/native_tool_stream.py +693 -0
- free_claude_code/providers/nvidia_nim/request_options.py +141 -0
- free_claude_code/providers/nvidia_nim/retry.py +85 -0
- free_claude_code/providers/nvidia_nim/tool_schema.py +255 -0
- free_claude_code/providers/nvidia_nim/voice.py +107 -0
- free_claude_code/providers/open_router/__init__.py +5 -0
- free_claude_code/providers/open_router/client.py +113 -0
- free_claude_code/providers/openai_api/__init__.py +5 -0
- free_claude_code/providers/openai_api/provider.py +150 -0
- free_claude_code/providers/openai_chat/__init__.py +72 -0
- free_claude_code/providers/openai_chat/base_url.py +7 -0
- free_claude_code/providers/openai_chat/behavior.py +133 -0
- free_claude_code/providers/openai_chat/client.py +40 -0
- free_claude_code/providers/openai_chat/extra_body.py +63 -0
- free_claude_code/providers/openai_chat/output_cap.py +161 -0
- free_claude_code/providers/openai_chat/profiles.py +778 -0
- free_claude_code/providers/openai_chat/provider.py +244 -0
- free_claude_code/providers/openai_chat/reasoning.py +169 -0
- free_claude_code/providers/openai_chat/reasoning_details.py +128 -0
- free_claude_code/providers/openai_chat/request_policy.py +140 -0
- free_claude_code/providers/openai_chat/stream_output.py +752 -0
- free_claude_code/providers/openai_chat/tool_calls.py +462 -0
- free_claude_code/providers/openai_chat/transport.py +1643 -0
- free_claude_code/providers/openai_chat/usage.py +117 -0
- free_claude_code/providers/openai_client.py +74 -0
- free_claude_code/providers/openai_codex/__init__.py +1 -0
- free_claude_code/providers/openai_codex/auth.py +626 -0
- free_claude_code/providers/openai_codex/endpoint.py +53 -0
- free_claude_code/providers/openai_codex/login.py +265 -0
- free_claude_code/providers/openai_codex/provider.py +259 -0
- free_claude_code/providers/openai_responses/__init__.py +5 -0
- free_claude_code/providers/openai_responses/presentation.py +115 -0
- free_claude_code/providers/openai_responses/transport.py +621 -0
- free_claude_code/providers/openai_stream.py +21 -0
- free_claude_code/providers/opencode/__init__.py +5 -0
- free_claude_code/providers/opencode/catalog.py +340 -0
- free_claude_code/providers/opencode/provider.py +359 -0
- free_claude_code/providers/reasoning_compatibility.py +199 -0
- free_claude_code/providers/request_recovery.py +122 -0
- free_claude_code/providers/runtime/__init__.py +10 -0
- free_claude_code/providers/runtime/config.py +91 -0
- free_claude_code/providers/runtime/discovery.py +63 -0
- free_claude_code/providers/runtime/factory.py +282 -0
- free_claude_code/providers/runtime/model_cache.py +93 -0
- free_claude_code/providers/runtime/runtime.py +108 -0
- free_claude_code/providers/stream_recovery.py +164 -0
- free_claude_code/providers/vertex/__init__.py +5 -0
- free_claude_code/providers/vertex/auth.py +105 -0
- free_claude_code/providers/vertex/client.py +140 -0
- free_claude_code/providers/vertex/endpoint.py +50 -0
- free_claude_code/providers/vertex/models.py +52 -0
- free_claude_code/runtime/__init__.py +1 -0
- free_claude_code/runtime/application.py +1121 -0
- free_claude_code/runtime/asgi.py +63 -0
- free_claude_code/runtime/bootstrap.py +153 -0
- free_claude_code/runtime/code_sessions_sqlite.py +785 -0
- free_claude_code/runtime/codex_app_server.py +775 -0
- free_claude_code/runtime/codex_catalog.py +62 -0
- free_claude_code/runtime/codex_protocol.py +662 -0
- free_claude_code/runtime/configuration.py +53 -0
- free_claude_code/runtime/folder_picker.py +134 -0
- free_claude_code/runtime/native_folder_dialog.py +123 -0
- free_claude_code/runtime/provider_manager.py +716 -0
- free_claude_code/runtime/retired_chat.py +47 -0
- free_claude_code/runtime/web_tools/__init__.py +1 -0
- free_claude_code/runtime/web_tools/client.py +238 -0
- free_claude_code/runtime/web_tools/constants.py +17 -0
- free_claude_code/runtime/web_tools/egress.py +89 -0
- free_claude_code/runtime/web_tools/parsers.py +78 -0
- free_claude_code-6.2.64.data/scripts/fcc-update +4 -0
- free_claude_code-6.2.64.data/scripts/fcc-update.cmd +5 -0
- free_claude_code-6.2.64.dist-info/METADATA +555 -0
- free_claude_code-6.2.64.dist-info/RECORD +429 -0
- free_claude_code-6.2.64.dist-info/WHEEL +4 -0
- free_claude_code-6.2.64.dist-info/entry_points.txt +15 -0
- free_claude_code-6.2.64.dist-info/licenses/LICENSE +239 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Remove saved data belonging to the retired Admin Chat Sessions feature."""
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
from free_claude_code.config import paths
|
|
6
|
+
from free_claude_code.core.interprocess_lock import InterprocessFileLock
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def remove_retired_chat_history() -> None:
|
|
10
|
+
"""Delete only owned history files, deferring failures to the next startup."""
|
|
11
|
+
try:
|
|
12
|
+
directory = paths.config_dir_path().resolve() / "chat"
|
|
13
|
+
try:
|
|
14
|
+
resolved = directory.resolve(strict=True)
|
|
15
|
+
except FileNotFoundError:
|
|
16
|
+
return
|
|
17
|
+
if resolved != directory:
|
|
18
|
+
logger.warning("Chat history cleanup deferred: redirected directory")
|
|
19
|
+
return
|
|
20
|
+
files = tuple(
|
|
21
|
+
directory / name
|
|
22
|
+
for name in ("chat.db", "chat.db-wal", "chat.db-shm", "chat.db-journal")
|
|
23
|
+
)
|
|
24
|
+
for path in files:
|
|
25
|
+
try:
|
|
26
|
+
path.lstat()
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
continue
|
|
29
|
+
break
|
|
30
|
+
else:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
lock = InterprocessFileLock(directory / "chat.lock")
|
|
34
|
+
try:
|
|
35
|
+
if not lock.acquire():
|
|
36
|
+
logger.warning("Chat history cleanup deferred: history is in use")
|
|
37
|
+
return
|
|
38
|
+
for path in files:
|
|
39
|
+
path.unlink(missing_ok=True)
|
|
40
|
+
finally:
|
|
41
|
+
# Keep the lock file: unlinking it could create two independent locks.
|
|
42
|
+
lock.release()
|
|
43
|
+
except OSError as exc:
|
|
44
|
+
logger.warning(
|
|
45
|
+
"Chat history cleanup deferred until next startup: exc_type={}",
|
|
46
|
+
type(exc).__name__,
|
|
47
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Outbound HTTP implementation for local web tools."""
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Outbound HTTP for web_search / web_fetch with operation-scoped resources."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import socket
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from urllib.parse import urljoin, urlparse
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
import httpx
|
|
10
|
+
from aiohttp import ClientSession, ClientTimeout, TCPConnector
|
|
11
|
+
from aiohttp.abc import AbstractResolver, ResolveResult
|
|
12
|
+
|
|
13
|
+
from free_claude_code.application.web_tools.ports import (
|
|
14
|
+
WebFetchEgressPolicy,
|
|
15
|
+
WebFetchEgressViolation,
|
|
16
|
+
)
|
|
17
|
+
from free_claude_code.core.web_tools import WebFetchResult, WebSearchResult
|
|
18
|
+
|
|
19
|
+
from . import constants
|
|
20
|
+
from .constants import (
|
|
21
|
+
_MAX_FETCH_CHARS,
|
|
22
|
+
_MAX_SEARCH_RESULTS,
|
|
23
|
+
_REDIRECT_RESPONSE_BODY_CAP_BYTES,
|
|
24
|
+
_REQUEST_TIMEOUT_S,
|
|
25
|
+
_WEB_FETCH_REDIRECT_STATUSES,
|
|
26
|
+
_WEB_TOOL_HTTP_HEADERS,
|
|
27
|
+
)
|
|
28
|
+
from .egress import (
|
|
29
|
+
get_validated_stream_addrinfos_for_egress,
|
|
30
|
+
)
|
|
31
|
+
from .parsers import HTMLTextParser, SearchResultParser
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def _iter_response_body_under_cap(
|
|
35
|
+
response: httpx.Response, max_bytes: int
|
|
36
|
+
) -> AsyncIterator[bytes]:
|
|
37
|
+
if max_bytes <= 0:
|
|
38
|
+
return
|
|
39
|
+
received = 0
|
|
40
|
+
async for chunk in response.aiter_bytes(chunk_size=65_536):
|
|
41
|
+
if received >= max_bytes:
|
|
42
|
+
break
|
|
43
|
+
remaining = max_bytes - received
|
|
44
|
+
if len(chunk) <= remaining:
|
|
45
|
+
received += len(chunk)
|
|
46
|
+
yield chunk
|
|
47
|
+
if received >= max_bytes:
|
|
48
|
+
break
|
|
49
|
+
else:
|
|
50
|
+
yield chunk[:remaining]
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def _drain_response_body_capped(response: httpx.Response, max_bytes: int) -> None:
|
|
55
|
+
async for _ in _iter_response_body_under_cap(response, max_bytes):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _read_response_body_capped(response: httpx.Response, max_bytes: int) -> bytes:
|
|
60
|
+
return b"".join(
|
|
61
|
+
[piece async for piece in _iter_response_body_under_cap(response, max_bytes)]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_NUMERIC_RESOLVE_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV
|
|
66
|
+
_NAME_RESOLVE_FLAGS = socket.NI_NUMERICHOST | socket.NI_NUMERICSERV
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def getaddrinfo_rows_to_resolve_results(
|
|
70
|
+
host: str, addrinfos: list[tuple]
|
|
71
|
+
) -> list[ResolveResult]:
|
|
72
|
+
"""Map :func:`socket.getaddrinfo` rows to aiohttp :class:`ResolveResult` (ThreadedResolver logic)."""
|
|
73
|
+
out: list[ResolveResult] = []
|
|
74
|
+
for family, _type, proto, _canon, sockaddr in addrinfos:
|
|
75
|
+
if family == socket.AF_INET6:
|
|
76
|
+
if len(sockaddr) < 3:
|
|
77
|
+
continue
|
|
78
|
+
if sockaddr[3]:
|
|
79
|
+
resolved_host, port = socket.getnameinfo(sockaddr, _NAME_RESOLVE_FLAGS)
|
|
80
|
+
else:
|
|
81
|
+
resolved_host, port = sockaddr[:2]
|
|
82
|
+
else:
|
|
83
|
+
assert family == socket.AF_INET, family
|
|
84
|
+
resolved_host, port = sockaddr[0], sockaddr[1]
|
|
85
|
+
resolved_host = str(resolved_host)
|
|
86
|
+
port = int(port)
|
|
87
|
+
out.append(
|
|
88
|
+
ResolveResult(
|
|
89
|
+
hostname=host,
|
|
90
|
+
host=resolved_host,
|
|
91
|
+
port=int(port),
|
|
92
|
+
family=family,
|
|
93
|
+
proto=proto,
|
|
94
|
+
flags=_NUMERIC_RESOLVE_FLAGS,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _PinnedEgressStaticResolver(AbstractResolver):
|
|
101
|
+
"""Return only pre-validated :class:`ResolveResult` for the outbound request."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, results: list[ResolveResult]) -> None:
|
|
104
|
+
self._results = results
|
|
105
|
+
|
|
106
|
+
async def resolve(
|
|
107
|
+
self, host: str, port: int = 0, family: int = socket.AF_INET
|
|
108
|
+
) -> list[ResolveResult]:
|
|
109
|
+
return self._results
|
|
110
|
+
|
|
111
|
+
async def close(self) -> None: # pragma: no cover - aiohttp contract
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def _read_aiohttp_body_capped(
|
|
116
|
+
response: aiohttp.ClientResponse, max_bytes: int
|
|
117
|
+
) -> bytes:
|
|
118
|
+
received = 0
|
|
119
|
+
parts: list[bytes] = []
|
|
120
|
+
async for chunk in response.content.iter_chunked(65_536):
|
|
121
|
+
if received >= max_bytes:
|
|
122
|
+
break
|
|
123
|
+
remaining = max_bytes - received
|
|
124
|
+
if len(chunk) <= remaining:
|
|
125
|
+
received += len(chunk)
|
|
126
|
+
parts.append(chunk)
|
|
127
|
+
else:
|
|
128
|
+
parts.append(chunk[:remaining])
|
|
129
|
+
break
|
|
130
|
+
return b"".join(parts)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def _drain_aiohttp_body_capped(
|
|
134
|
+
response: aiohttp.ClientResponse, max_bytes: int
|
|
135
|
+
) -> None:
|
|
136
|
+
if max_bytes <= 0:
|
|
137
|
+
return
|
|
138
|
+
received = 0
|
|
139
|
+
async for chunk in response.content.iter_chunked(65_536):
|
|
140
|
+
received += len(chunk)
|
|
141
|
+
if received >= max_bytes:
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class HTTPWebToolsClient:
|
|
146
|
+
"""Stateless adapter; each call owns its HTTP resources until completion."""
|
|
147
|
+
|
|
148
|
+
async def search(self, query: str) -> list[WebSearchResult]:
|
|
149
|
+
async with (
|
|
150
|
+
httpx.AsyncClient(
|
|
151
|
+
timeout=_REQUEST_TIMEOUT_S,
|
|
152
|
+
follow_redirects=True,
|
|
153
|
+
headers=_WEB_TOOL_HTTP_HEADERS,
|
|
154
|
+
) as client,
|
|
155
|
+
client.stream(
|
|
156
|
+
"GET",
|
|
157
|
+
"https://lite.duckduckgo.com/lite/",
|
|
158
|
+
params={"q": query},
|
|
159
|
+
) as response,
|
|
160
|
+
):
|
|
161
|
+
response.raise_for_status()
|
|
162
|
+
body_bytes = await _read_response_body_capped(
|
|
163
|
+
response, constants._MAX_WEB_FETCH_RESPONSE_BYTES
|
|
164
|
+
)
|
|
165
|
+
text = body_bytes.decode("utf-8", errors="replace")
|
|
166
|
+
parser = SearchResultParser()
|
|
167
|
+
parser.feed(text)
|
|
168
|
+
return parser.results[:_MAX_SEARCH_RESULTS]
|
|
169
|
+
|
|
170
|
+
async def fetch(self, url: str, *, egress: WebFetchEgressPolicy) -> WebFetchResult:
|
|
171
|
+
"""Fetch URL with manual redirects; each hop is DNS-pinned to validated addresses."""
|
|
172
|
+
current_url = url
|
|
173
|
+
redirect_hops = 0
|
|
174
|
+
timeout = ClientTimeout(total=_REQUEST_TIMEOUT_S)
|
|
175
|
+
|
|
176
|
+
while True:
|
|
177
|
+
addr_infos = await asyncio.to_thread(
|
|
178
|
+
get_validated_stream_addrinfos_for_egress, current_url, egress
|
|
179
|
+
)
|
|
180
|
+
host = urlparse(current_url).hostname or ""
|
|
181
|
+
results = getaddrinfo_rows_to_resolve_results(host, addr_infos)
|
|
182
|
+
resolver = _PinnedEgressStaticResolver(results)
|
|
183
|
+
connector = TCPConnector(
|
|
184
|
+
resolver=resolver,
|
|
185
|
+
force_close=True,
|
|
186
|
+
)
|
|
187
|
+
try:
|
|
188
|
+
async with (
|
|
189
|
+
ClientSession(
|
|
190
|
+
timeout=timeout,
|
|
191
|
+
headers=_WEB_TOOL_HTTP_HEADERS,
|
|
192
|
+
connector=connector,
|
|
193
|
+
) as session,
|
|
194
|
+
session.get(current_url, allow_redirects=False) as response,
|
|
195
|
+
):
|
|
196
|
+
if response.status in _WEB_FETCH_REDIRECT_STATUSES:
|
|
197
|
+
await _drain_aiohttp_body_capped(
|
|
198
|
+
response, _REDIRECT_RESPONSE_BODY_CAP_BYTES
|
|
199
|
+
)
|
|
200
|
+
if redirect_hops >= constants._MAX_WEB_FETCH_REDIRECTS:
|
|
201
|
+
raise WebFetchEgressViolation(
|
|
202
|
+
"web_fetch exceeded maximum redirects "
|
|
203
|
+
f"({constants._MAX_WEB_FETCH_REDIRECTS})"
|
|
204
|
+
)
|
|
205
|
+
location = response.headers.get("location")
|
|
206
|
+
if not location or not location.strip():
|
|
207
|
+
raise WebFetchEgressViolation(
|
|
208
|
+
"web_fetch redirect response missing Location header"
|
|
209
|
+
)
|
|
210
|
+
current_url = urljoin(str(response.url), location.strip())
|
|
211
|
+
redirect_hops += 1
|
|
212
|
+
continue
|
|
213
|
+
response.raise_for_status()
|
|
214
|
+
content_type = response.headers.get("content-type", "text/plain")
|
|
215
|
+
final_url = str(response.url)
|
|
216
|
+
encoding = response.get_encoding() or "utf-8"
|
|
217
|
+
body_bytes = await _read_aiohttp_body_capped(
|
|
218
|
+
response, constants._MAX_WEB_FETCH_RESPONSE_BYTES
|
|
219
|
+
)
|
|
220
|
+
finally:
|
|
221
|
+
await connector.close()
|
|
222
|
+
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
text = body_bytes.decode(encoding, errors="replace")
|
|
226
|
+
title = final_url
|
|
227
|
+
data = text
|
|
228
|
+
if "html" in content_type.lower():
|
|
229
|
+
parser = HTMLTextParser()
|
|
230
|
+
parser.feed(text)
|
|
231
|
+
title = parser.title or final_url
|
|
232
|
+
data = "\n".join(parser.text_parts)
|
|
233
|
+
return WebFetchResult(
|
|
234
|
+
url=final_url,
|
|
235
|
+
title=title,
|
|
236
|
+
media_type="text/plain",
|
|
237
|
+
data=data[:_MAX_FETCH_CHARS],
|
|
238
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Limits and defaults for outbound web server tool HTTP."""
|
|
2
|
+
|
|
3
|
+
from free_claude_code.core.version import package_version
|
|
4
|
+
|
|
5
|
+
_REQUEST_TIMEOUT_S = 20.0
|
|
6
|
+
_MAX_SEARCH_RESULTS = 10
|
|
7
|
+
_MAX_FETCH_CHARS = 24_000
|
|
8
|
+
# Hard cap on raw bytes read from HTTP responses before decode / HTML parse (memory bound).
|
|
9
|
+
_MAX_WEB_FETCH_RESPONSE_BYTES = 2 * 1024 * 1024
|
|
10
|
+
# Drain at most this many bytes from redirect responses before following Location.
|
|
11
|
+
_REDIRECT_RESPONSE_BODY_CAP_BYTES = 65_536
|
|
12
|
+
_MAX_WEB_FETCH_REDIRECTS = 10
|
|
13
|
+
_WEB_FETCH_REDIRECT_STATUSES = frozenset({301, 302, 303, 307, 308})
|
|
14
|
+
|
|
15
|
+
_WEB_TOOL_HTTP_HEADERS = {
|
|
16
|
+
"User-Agent": f"Mozilla/5.0 compatible; free-claude-code/{package_version()}",
|
|
17
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Egress policy for user-controlled web_fetch URLs (SSRF guard)."""
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
import socket
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
from free_claude_code.application.web_tools.ports import (
|
|
8
|
+
WebFetchEgressPolicy,
|
|
9
|
+
WebFetchEgressViolation,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _port_for_url(parsed) -> int:
|
|
14
|
+
if parsed.port is not None:
|
|
15
|
+
return parsed.port
|
|
16
|
+
return 443 if (parsed.scheme or "").lower() == "https" else 80
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _stream_getaddrinfo_or_raise(host: str, port: int) -> list[tuple]:
|
|
20
|
+
try:
|
|
21
|
+
return socket.getaddrinfo(
|
|
22
|
+
host, port, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP
|
|
23
|
+
)
|
|
24
|
+
except OSError as exc:
|
|
25
|
+
raise WebFetchEgressViolation(
|
|
26
|
+
f"Could not resolve host {host!r}: {exc}"
|
|
27
|
+
) from exc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_validated_stream_addrinfos_for_egress(
|
|
31
|
+
url: str, policy: WebFetchEgressPolicy
|
|
32
|
+
) -> list[tuple]:
|
|
33
|
+
"""Resolve and validate a URL for web_fetch, returning getaddrinfo rows for pinning.
|
|
34
|
+
|
|
35
|
+
Each HTTP connect pins to only these `getaddrinfo` results so a malicious DNS
|
|
36
|
+
server cannot rebind to a disallowed address between resolution and the TCP
|
|
37
|
+
connect (used by :meth:`runtime.web_tools.client.HTTPWebToolsClient.fetch`).
|
|
38
|
+
"""
|
|
39
|
+
parsed = urlparse(url)
|
|
40
|
+
scheme = (parsed.scheme or "").lower()
|
|
41
|
+
if scheme not in policy.allowed_schemes:
|
|
42
|
+
raise WebFetchEgressViolation(
|
|
43
|
+
f"URL scheme {scheme!r} is not allowed for web_fetch"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
host = parsed.hostname
|
|
47
|
+
if host is None or host == "":
|
|
48
|
+
raise WebFetchEgressViolation("web_fetch URL must include a host")
|
|
49
|
+
|
|
50
|
+
port = _port_for_url(parsed)
|
|
51
|
+
|
|
52
|
+
if policy.allow_private_network_targets:
|
|
53
|
+
return _stream_getaddrinfo_or_raise(host, port)
|
|
54
|
+
|
|
55
|
+
host_lower = host.lower()
|
|
56
|
+
if host_lower == "localhost" or host_lower.endswith(".localhost"):
|
|
57
|
+
raise WebFetchEgressViolation("localhost targets are not allowed for web_fetch")
|
|
58
|
+
if host_lower.endswith(".local"):
|
|
59
|
+
raise WebFetchEgressViolation(".local hostnames are not allowed for web_fetch")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
parsed_ip = ipaddress.ip_address(host)
|
|
63
|
+
except ValueError:
|
|
64
|
+
parsed_ip = None
|
|
65
|
+
|
|
66
|
+
if parsed_ip is not None:
|
|
67
|
+
if not parsed_ip.is_global:
|
|
68
|
+
raise WebFetchEgressViolation(
|
|
69
|
+
f"Non-public IP host {host!r} is not allowed for web_fetch"
|
|
70
|
+
)
|
|
71
|
+
return _stream_getaddrinfo_or_raise(host, port)
|
|
72
|
+
|
|
73
|
+
infos = _stream_getaddrinfo_or_raise(host, port)
|
|
74
|
+
for *_, sockaddr in infos:
|
|
75
|
+
addr = sockaddr[0]
|
|
76
|
+
try:
|
|
77
|
+
resolved = ipaddress.ip_address(addr)
|
|
78
|
+
except ValueError:
|
|
79
|
+
continue
|
|
80
|
+
if not resolved.is_global:
|
|
81
|
+
raise WebFetchEgressViolation(
|
|
82
|
+
f"Host {host!r} resolves to a non-public address ({resolved})"
|
|
83
|
+
)
|
|
84
|
+
return infos
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def enforce_web_fetch_egress(url: str, policy: WebFetchEgressPolicy) -> None:
|
|
88
|
+
"""Validate ``url`` (scheme, host, and resolved addresses) for web_fetch."""
|
|
89
|
+
get_validated_stream_addrinfos_for_egress(url, policy)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""HTML parsing for web_search / web_fetch."""
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
from html.parser import HTMLParser
|
|
5
|
+
from urllib.parse import parse_qs, unquote, urlparse
|
|
6
|
+
|
|
7
|
+
from free_claude_code.core.web_tools import WebSearchResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchResultParser(HTMLParser):
|
|
11
|
+
"""DuckDuckGo lite HTML: extract result links and titles."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.results: list[WebSearchResult] = []
|
|
16
|
+
self._href: str | None = None
|
|
17
|
+
self._title_parts: list[str] = []
|
|
18
|
+
|
|
19
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
20
|
+
if tag != "a":
|
|
21
|
+
return
|
|
22
|
+
href = dict(attrs).get("href")
|
|
23
|
+
if not href or "uddg=" not in href:
|
|
24
|
+
return
|
|
25
|
+
parsed = urlparse(href)
|
|
26
|
+
query = parse_qs(parsed.query)
|
|
27
|
+
uddg = query.get("uddg", [""])[0]
|
|
28
|
+
if not uddg:
|
|
29
|
+
return
|
|
30
|
+
self._href = unquote(uddg)
|
|
31
|
+
self._title_parts = []
|
|
32
|
+
|
|
33
|
+
def handle_data(self, data: str) -> None:
|
|
34
|
+
if self._href is not None:
|
|
35
|
+
self._title_parts.append(data)
|
|
36
|
+
|
|
37
|
+
def handle_endtag(self, tag: str) -> None:
|
|
38
|
+
if tag != "a" or self._href is None:
|
|
39
|
+
return
|
|
40
|
+
title = " ".join("".join(self._title_parts).split())
|
|
41
|
+
if title and not any(result.url == self._href for result in self.results):
|
|
42
|
+
self.results.append(
|
|
43
|
+
WebSearchResult(title=html.unescape(title), url=self._href)
|
|
44
|
+
)
|
|
45
|
+
self._href = None
|
|
46
|
+
self._title_parts = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HTMLTextParser(HTMLParser):
|
|
50
|
+
"""Strip scripts/styles and collect visible text + title for fetch previews."""
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
super().__init__()
|
|
54
|
+
self.title = ""
|
|
55
|
+
self.text_parts: list[str] = []
|
|
56
|
+
self._in_title = False
|
|
57
|
+
self._skip_depth = 0
|
|
58
|
+
|
|
59
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
60
|
+
if tag in {"script", "style", "noscript"}:
|
|
61
|
+
self._skip_depth += 1
|
|
62
|
+
elif tag == "title":
|
|
63
|
+
self._in_title = True
|
|
64
|
+
|
|
65
|
+
def handle_endtag(self, tag: str) -> None:
|
|
66
|
+
if tag in {"script", "style", "noscript"} and self._skip_depth:
|
|
67
|
+
self._skip_depth -= 1
|
|
68
|
+
elif tag == "title":
|
|
69
|
+
self._in_title = False
|
|
70
|
+
|
|
71
|
+
def handle_data(self, data: str) -> None:
|
|
72
|
+
text = " ".join(data.split())
|
|
73
|
+
if not text:
|
|
74
|
+
return
|
|
75
|
+
if self._in_title:
|
|
76
|
+
self.title = f"{self.title} {text}".strip()
|
|
77
|
+
elif not self._skip_depth:
|
|
78
|
+
self.text_parts.append(text)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
(
|
|
3
|
+
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; & ([scriptblock]::Create((Invoke-RestMethod 'https://raw.githubusercontent.com/Alishahryar1/free-claude-code/main/scripts/install.ps1')))" %*
|
|
4
|
+
call exit /b %%errorlevel%%
|
|
5
|
+
)
|