tokenade 1.0.0__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.
- tokenade/__init__.py +84 -0
- tokenade/__main__.py +5 -0
- tokenade/cli/__init__.py +1937 -0
- tokenade/cli/__main__.py +5 -0
- tokenade/cli/advanced.py +490 -0
- tokenade/cli/completions.py +73 -0
- tokenade/cli/config.py +47 -0
- tokenade/cli/handlers/__init__.py +51 -0
- tokenade/cli/handlers/browser_ops.py +1373 -0
- tokenade/cli/handlers/ci.py +361 -0
- tokenade/cli/handlers/infrastructure.py +452 -0
- tokenade/cli/handlers/misc.py +222 -0
- tokenade/cli/handlers/session_ops.py +1263 -0
- tokenade/cli/management.py +82 -0
- tokenade/cli/output.py +176 -0
- tokenade/cli/proxy.py +279 -0
- tokenade/cli/security.py +167 -0
- tokenade/cli/session.py +885 -0
- tokenade/core/__init__.py +0 -0
- tokenade/core/antidetection/__init__.py +4 -0
- tokenade/core/antidetection/behavioral.py +140 -0
- tokenade/core/antidetection/cdp_cleaner.py +60 -0
- tokenade/core/api/__init__.py +3 -0
- tokenade/core/api/server.py +445 -0
- tokenade/core/batch/__init__.py +23 -0
- tokenade/core/batch/operations.py +398 -0
- tokenade/core/browser/__init__.py +84 -0
- tokenade/core/browser/battle.py +865 -0
- tokenade/core/browser/captcha.py +244 -0
- tokenade/core/browser/cdp_connection.py +1095 -0
- tokenade/core/browser/cloak.py +24 -0
- tokenade/core/browser/cloudflare.py +294 -0
- tokenade/core/browser/dashboard.py +195 -0
- tokenade/core/browser/dependencies.py +271 -0
- tokenade/core/browser/fingerprint.py +262 -0
- tokenade/core/browser/manager.py +317 -0
- tokenade/core/browser/patcher.py +322 -0
- tokenade/core/browser/profile_cloner.py +344 -0
- tokenade/core/browser/profiles.py +221 -0
- tokenade/core/browser/session_state.py +254 -0
- tokenade/core/browser/stealth/__init__.py +68 -0
- tokenade/core/browser/stealth/backend.py +101 -0
- tokenade/core/browser/stealth/cloak.py +258 -0
- tokenade/core/browser/stealth/launcher.py +593 -0
- tokenade/core/browser/stealth/manager.py +671 -0
- tokenade/core/browser/stealth.py +24 -0
- tokenade/core/browser/stealth_test.py +324 -0
- tokenade/core/browser/storage_extractor.py +231 -0
- tokenade/core/browser/synchronizer.py +200 -0
- tokenade/core/browser/tls_fingerprint.py +179 -0
- tokenade/core/browser/undetectable.py +16 -0
- tokenade/core/browser/xvfb.py +222 -0
- tokenade/core/cicd/__init__.py +29 -0
- tokenade/core/cicd/runner.py +548 -0
- tokenade/core/cicd/workflow_generator.py +432 -0
- tokenade/core/config.py +103 -0
- tokenade/core/crypto/__init__.py +19 -0
- tokenade/core/crypto/at_rest.py +178 -0
- tokenade/core/crypto/cookie_crypto.py +805 -0
- tokenade/core/crypto/encryptor.py +301 -0
- tokenade/core/daemon/__init__.py +4 -0
- tokenade/core/daemon/session_daemon.py +675 -0
- tokenade/core/errors.py +77 -0
- tokenade/core/fingerprint/__init__.py +0 -0
- tokenade/core/fingerprint/collectors/__init__.py +30 -0
- tokenade/core/fingerprint/collectors/audio.py +124 -0
- tokenade/core/fingerprint/collectors/base.py +66 -0
- tokenade/core/fingerprint/collectors/battery.py +59 -0
- tokenade/core/fingerprint/collectors/canvas.py +81 -0
- tokenade/core/fingerprint/collectors/fonts.py +52 -0
- tokenade/core/fingerprint/collectors/navigator.py +99 -0
- tokenade/core/fingerprint/collectors/plugins.py +92 -0
- tokenade/core/fingerprint/collectors/screen.py +81 -0
- tokenade/core/fingerprint/collectors/webgl.py +104 -0
- tokenade/core/fingerprint/collectors/webrtc.py +91 -0
- tokenade/core/fingerprint/injector.py +94 -0
- tokenade/core/fingerprint/manager.py +278 -0
- tokenade/core/fingerprint/stealth.py +357 -0
- tokenade/core/forensics/__init__.py +15 -0
- tokenade/core/forensics/autopsy.py +464 -0
- tokenade/core/importer/__init__.py +23 -0
- tokenade/core/importer/adb_extractor.py +259 -0
- tokenade/core/importer/advanced_validator.py +665 -0
- tokenade/core/importer/browser_discovery.py +313 -0
- tokenade/core/importer/chromium_forks.py +174 -0
- tokenade/core/importer/competitor_import.py +324 -0
- tokenade/core/importer/cookie_extractor.py +612 -0
- tokenade/core/importer/db_utils.py +66 -0
- tokenade/core/importer/format_exporter.py +210 -0
- tokenade/core/importer/format_importer.py +277 -0
- tokenade/core/importer/local_storage_extractor.py +271 -0
- tokenade/core/importer/mobile_extractor.py +263 -0
- tokenade/core/importer/mobile_import.py +556 -0
- tokenade/core/importer/plugin_export.py +321 -0
- tokenade/core/importer/safari_extractor.py +376 -0
- tokenade/core/importer/session_comparator.py +126 -0
- tokenade/core/importer/session_loader.py +494 -0
- tokenade/core/importer/session_manager.py +295 -0
- tokenade/core/importer/session_packager.py +492 -0
- tokenade/core/importer/session_refresher.py +467 -0
- tokenade/core/importer/session_rotation.py +147 -0
- tokenade/core/importer/session_rotator.py +279 -0
- tokenade/core/importer/session_sharer.py +713 -0
- tokenade/core/importer/session_sync.py +298 -0
- tokenade/core/importer/session_vault.py +282 -0
- tokenade/core/importer/site_configs.py +314 -0
- tokenade/core/importer/tor_extractor.py +138 -0
- tokenade/core/importer/validator.py +497 -0
- tokenade/core/injector/__init__.py +15 -0
- tokenade/core/injector/profile_manager.py +386 -0
- tokenade/core/integration/__init__.py +32 -0
- tokenade/core/integration/container_orchestrator.py +512 -0
- tokenade/core/integration/docker_manager.py +349 -0
- tokenade/core/integration/fleet.py +464 -0
- tokenade/core/integration/kubernetes.py +393 -0
- tokenade/core/integration/plugin_browser.py +251 -0
- tokenade/core/integration/plugin_loader.py +395 -0
- tokenade/core/integration/plugin_registry.py +620 -0
- tokenade/core/integration/plugin_search.py +186 -0
- tokenade/core/integration/plugin_testing.py +422 -0
- tokenade/core/integration/plugin_verifier.py +193 -0
- tokenade/core/integration/rating_sync.py +286 -0
- tokenade/core/integration/webhooks.py +219 -0
- tokenade/core/logging/__init__.py +6 -0
- tokenade/core/logging/structured.py +182 -0
- tokenade/core/monitoring/__init__.py +0 -0
- tokenade/core/monitoring/analytics.py +194 -0
- tokenade/core/monitoring/session_monitor.py +479 -0
- tokenade/core/proxy/__init__.py +42 -0
- tokenade/core/proxy/cdp_api.py +97 -0
- tokenade/core/proxy/cdp_gui.py +349 -0
- tokenade/core/proxy/cdp_injection.py +376 -0
- tokenade/core/proxy/cdp_proxy.py +808 -0
- tokenade/core/proxy/cdp_routing.py +419 -0
- tokenade/core/proxy/cdp_stealth.py +229 -0
- tokenade/core/proxy/extension_bridge.py +132 -0
- tokenade/core/proxy/forward_proxy.py +317 -0
- tokenade/core/proxy/manager.py +275 -0
- tokenade/core/proxy/multi_site_proxy.py +255 -0
- tokenade/core/proxy/residential.py +305 -0
- tokenade/core/proxy/rotation.py +365 -0
- tokenade/core/proxy/server.py +422 -0
- tokenade/core/proxy/server_gui.py +214 -0
- tokenade/core/proxy/server_routing.py +128 -0
- tokenade/core/proxy/server_utils.py +253 -0
- tokenade/core/refresh/__init__.py +67 -0
- tokenade/core/refresh/batch_refresh.py +319 -0
- tokenade/core/refresh/encrypted_refresh.py +342 -0
- tokenade/core/refresh/health_checker.py +355 -0
- tokenade/core/refresh/health_reporter.py +379 -0
- tokenade/core/refresh/health_scorer.py +292 -0
- tokenade/core/refresh/oauth_refresh.py +611 -0
- tokenade/core/refresh/session_validator.py +367 -0
- tokenade/core/runtime/__init__.py +30 -0
- tokenade/core/runtime/engine.py +783 -0
- tokenade/core/runtime/tls_matcher.py +282 -0
- tokenade/core/security/__init__.py +27 -0
- tokenade/core/security/audit.py +577 -0
- tokenade/core/security/credentials.py +417 -0
- tokenade/core/storage/__init__.py +1 -0
- tokenade/core/storage/session_versions.py +261 -0
- tokenade/core/utils/__init__.py +0 -0
- tokenade/core/utils/performance.py +446 -0
- tokenade/handlers/__init__.py +42 -0
- tokenade/handlers/base.py +288 -0
- tokenade/handlers/generic_oauth.py +593 -0
- tokenade/handlers/github.py +347 -0
- tokenade/handlers/google.py +301 -0
- tokenade/handlers/resolve.py +49 -0
- tokenade/plugin/__init__.py +62 -0
- tokenade/plugin/api.py +139 -0
- tokenade/plugin/base.py +681 -0
- tokenade/plugin/oauth2/__init__.py +4 -0
- tokenade/plugin/oauth2/plugin.py +282 -0
- tokenade/sdk/__init__.py +257 -0
- tokenade/tests/__init__.py +0 -0
- tokenade/tests/portability.py +308 -0
- tokenade/tests/test_accounts.py +286 -0
- tokenade/tests/test_advanced_validator.py +209 -0
- tokenade/tests/test_advanced_validator_edge_cases.py +1115 -0
- tokenade/tests/test_analytics.py +222 -0
- tokenade/tests/test_antidetection.py +145 -0
- tokenade/tests/test_api_monitor_endpoints.py +204 -0
- tokenade/tests/test_api_monitoring.py +26 -0
- tokenade/tests/test_api_sdk.py +262 -0
- tokenade/tests/test_api_server_comprehensive.py +400 -0
- tokenade/tests/test_api_server_coverage.py +232 -0
- tokenade/tests/test_at_rest_encryption.py +210 -0
- tokenade/tests/test_audit_coverage.py +560 -0
- tokenade/tests/test_autopsy.py +375 -0
- tokenade/tests/test_base_handler_coverage.py +365 -0
- tokenade/tests/test_batch_operations.py +311 -0
- tokenade/tests/test_batch_operations_coverage.py +415 -0
- tokenade/tests/test_batch_refresh.py +175 -0
- tokenade/tests/test_battle.py +274 -0
- tokenade/tests/test_benchmarks.py +209 -0
- tokenade/tests/test_binary_patcher.py +419 -0
- tokenade/tests/test_browser.py +367 -0
- tokenade/tests/test_browser_discovery.py +58 -0
- tokenade/tests/test_browser_discovery_coverage.py +516 -0
- tokenade/tests/test_browser_improvements.py +277 -0
- tokenade/tests/test_browser_manager_coverage.py +764 -0
- tokenade/tests/test_browser_support.py +728 -0
- tokenade/tests/test_cdp_gui_coverage.py +573 -0
- tokenade/tests/test_cdp_injection_coverage.py +1056 -0
- tokenade/tests/test_cdp_injection_edge_cases.py +396 -0
- tokenade/tests/test_cdp_proxy_coverage.py +534 -0
- tokenade/tests/test_cdp_routing_coverage.py +1162 -0
- tokenade/tests/test_cdp_stealth_urls.py +50 -0
- tokenade/tests/test_chromium_forks_coverage.py +411 -0
- tokenade/tests/test_ci_runner.py +500 -0
- tokenade/tests/test_cicd.py +122 -0
- tokenade/tests/test_cli.py +200 -0
- tokenade/tests/test_cli_advanced.py +943 -0
- tokenade/tests/test_cli_helpers.py +99 -0
- tokenade/tests/test_cli_init_coverage.py +651 -0
- tokenade/tests/test_cli_integration.py +205 -0
- tokenade/tests/test_cli_management.py +231 -0
- tokenade/tests/test_cli_management_coverage.py +419 -0
- tokenade/tests/test_cli_monitor.py +201 -0
- tokenade/tests/test_cli_output.py +219 -0
- tokenade/tests/test_cli_proxy.py +1004 -0
- tokenade/tests/test_cli_refactor.py +454 -0
- tokenade/tests/test_cli_security_coverage.py +306 -0
- tokenade/tests/test_cli_security_proxy.py +242 -0
- tokenade/tests/test_cli_session.py +1168 -0
- tokenade/tests/test_cloak.py +375 -0
- tokenade/tests/test_cloudflare_akamai.py +384 -0
- tokenade/tests/test_collectors.py +283 -0
- tokenade/tests/test_comprehensive.py +311 -0
- tokenade/tests/test_config_and_cdp_modules.py +610 -0
- tokenade/tests/test_container_orchestration.py +488 -0
- tokenade/tests/test_cookie_crypto.py +835 -0
- tokenade/tests/test_cookie_extractor.py +1088 -0
- tokenade/tests/test_credentials_keyring_errors.py +68 -0
- tokenade/tests/test_crypto.py +188 -0
- tokenade/tests/test_custom_errors.py +100 -0
- tokenade/tests/test_daemon.py +430 -0
- tokenade/tests/test_db_utils.py +93 -0
- tokenade/tests/test_db_utils_coverage.py +218 -0
- tokenade/tests/test_docker_manager.py +587 -0
- tokenade/tests/test_e2e_headless.py +207 -0
- tokenade/tests/test_encrypted_refresh.py +169 -0
- tokenade/tests/test_encryptor.py +323 -0
- tokenade/tests/test_encryptor_coverage.py +339 -0
- tokenade/tests/test_encryptor_edge.py +66 -0
- tokenade/tests/test_engine_coverage.py +748 -0
- tokenade/tests/test_enterprise.py +819 -0
- tokenade/tests/test_errors.py +49 -0
- tokenade/tests/test_extension_bridge_coverage.py +215 -0
- tokenade/tests/test_fingerprint.py +66 -0
- tokenade/tests/test_fingerprint_collectors.py +397 -0
- tokenade/tests/test_fingerprint_collectors_coverage.py +298 -0
- tokenade/tests/test_fingerprint_consistency.py +202 -0
- tokenade/tests/test_fingerprint_injector.py +308 -0
- tokenade/tests/test_fingerprint_manager.py +580 -0
- tokenade/tests/test_fleet.py +398 -0
- tokenade/tests/test_format_export.py +414 -0
- tokenade/tests/test_format_importer_and_helpers.py +656 -0
- tokenade/tests/test_forward_proxy.py +52 -0
- tokenade/tests/test_forward_proxy_coverage.py +725 -0
- tokenade/tests/test_generic_oauth_coverage.py +576 -0
- tokenade/tests/test_github_handler_coverage.py +342 -0
- tokenade/tests/test_google_handler_coverage.py +317 -0
- tokenade/tests/test_handler_resolve.py +32 -0
- tokenade/tests/test_handlers.py +521 -0
- tokenade/tests/test_health_checker_coverage.py +451 -0
- tokenade/tests/test_health_reporter.py +287 -0
- tokenade/tests/test_health_scorer.py +537 -0
- tokenade/tests/test_importer_integration.py +290 -0
- tokenade/tests/test_integration.py +662 -0
- tokenade/tests/test_kubernetes.py +484 -0
- tokenade/tests/test_local_storage.py +56 -0
- tokenade/tests/test_local_storage_extractor.py +174 -0
- tokenade/tests/test_local_storage_extractor_coverage.py +291 -0
- tokenade/tests/test_local_storage_extractor_plyvel.py +400 -0
- tokenade/tests/test_mac_crypto.py +74 -0
- tokenade/tests/test_mobile_extractor_coverage.py +397 -0
- tokenade/tests/test_mobile_import.py +240 -0
- tokenade/tests/test_monitoring.py +92 -0
- tokenade/tests/test_multi_site_proxy.py +51 -0
- tokenade/tests/test_multi_site_proxy_coverage.py +408 -0
- tokenade/tests/test_mutmut_killers.py +122 -0
- tokenade/tests/test_new_plugins.py +295 -0
- tokenade/tests/test_oauth_refresh.py +412 -0
- tokenade/tests/test_official_plugin_contracts.py +52 -0
- tokenade/tests/test_performance.py +294 -0
- tokenade/tests/test_performance_coverage.py +392 -0
- tokenade/tests/test_phase80.py +266 -0
- tokenade/tests/test_playwright_e2e.py +663 -0
- tokenade/tests/test_plugin_api.py +347 -0
- tokenade/tests/test_plugin_api_v11.py +357 -0
- tokenade/tests/test_plugin_loader.py +297 -0
- tokenade/tests/test_plugin_loader_coverage.py +254 -0
- tokenade/tests/test_plugin_marketplace.py +838 -0
- tokenade/tests/test_plugin_registry.py +447 -0
- tokenade/tests/test_plugin_system.py +339 -0
- tokenade/tests/test_plugin_types.py +358 -0
- tokenade/tests/test_profile_cloner.py +226 -0
- tokenade/tests/test_profile_management.py +280 -0
- tokenade/tests/test_profile_manager.py +338 -0
- tokenade/tests/test_profile_manager_coverage.py +400 -0
- tokenade/tests/test_profile_manager_errors.py +397 -0
- tokenade/tests/test_property_based.py +214 -0
- tokenade/tests/test_proxy.py +652 -0
- tokenade/tests/test_proxy_config.py +86 -0
- tokenade/tests/test_proxy_gui.py +80 -0
- tokenade/tests/test_proxy_manager.py +218 -0
- tokenade/tests/test_proxy_modules.py +197 -0
- tokenade/tests/test_proxy_rotation.py +584 -0
- tokenade/tests/test_rating_sync.py +156 -0
- tokenade/tests/test_refresh_browser.py +511 -0
- tokenade/tests/test_runtime.py +474 -0
- tokenade/tests/test_safari_extractor_coverage.py +538 -0
- tokenade/tests/test_sdk_coverage.py +429 -0
- tokenade/tests/test_security.py +349 -0
- tokenade/tests/test_server_coverage.py +696 -0
- tokenade/tests/test_server_routing_coverage.py +349 -0
- tokenade/tests/test_server_utils_coverage.py +485 -0
- tokenade/tests/test_session_comparator.py +110 -0
- tokenade/tests/test_session_loader.py +52 -0
- tokenade/tests/test_session_loader_coverage.py +621 -0
- tokenade/tests/test_session_manager.py +135 -0
- tokenade/tests/test_session_monitor_coverage.py +297 -0
- tokenade/tests/test_session_monitor_enhanced.py +618 -0
- tokenade/tests/test_session_packager.py +104 -0
- tokenade/tests/test_session_packager_coverage.py +466 -0
- tokenade/tests/test_session_refresher.py +543 -0
- tokenade/tests/test_session_rotation.py +408 -0
- tokenade/tests/test_session_rotator.py +377 -0
- tokenade/tests/test_session_sharer.py +591 -0
- tokenade/tests/test_session_sharer_qr_versions.py +272 -0
- tokenade/tests/test_session_sync.py +598 -0
- tokenade/tests/test_session_sync_coverage.py +302 -0
- tokenade/tests/test_session_validator.py +204 -0
- tokenade/tests/test_session_vault.py +418 -0
- tokenade/tests/test_session_versions.py +262 -0
- tokenade/tests/test_share_improvements.py +181 -0
- tokenade/tests/test_site_configs.py +92 -0
- tokenade/tests/test_site_filter.py +65 -0
- tokenade/tests/test_site_handlers.py +318 -0
- tokenade/tests/test_ssrf.py +54 -0
- tokenade/tests/test_stealth.py +147 -0
- tokenade/tests/test_stealth_enhanced.py +444 -0
- tokenade/tests/test_stealth_validation.py +217 -0
- tokenade/tests/test_structured_logging.py +243 -0
- tokenade/tests/test_sync_import.py +302 -0
- tokenade/tests/test_tls.py +39 -0
- tokenade/tests/test_tls_matcher.py +133 -0
- tokenade/tests/test_tls_matcher_coverage.py +310 -0
- tokenade/tests/test_tor_extractor_coverage.py +141 -0
- tokenade/tests/test_tui.py +259 -0
- tokenade/tests/test_undetectable.py +176 -0
- tokenade/tests/test_validator_coverage.py +582 -0
- tokenade/tests/test_webhooks.py +290 -0
- tokenade/tests/test_xvfb.py +172 -0
- tokenade/tui/__init__.py +7 -0
- tokenade/tui/app.py +1145 -0
- tokenade-1.0.0.dist-info/METADATA +831 -0
- tokenade-1.0.0.dist-info/RECORD +363 -0
- tokenade-1.0.0.dist-info/WHEEL +5 -0
- tokenade-1.0.0.dist-info/entry_points.txt +2 -0
- tokenade-1.0.0.dist-info/top_level.txt +1 -0
tokenade/cli/__init__.py
ADDED
|
@@ -0,0 +1,1937 @@
|
|
|
1
|
+
"""Tokenade CLI - Main entry point and argument parser."""
|
|
2
|
+
import argparse
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from tokenade.cli.session import cmd_extract, cmd_export, cmd_load, cmd_transfer, cmd_inject_profile
|
|
9
|
+
from tokenade.cli.security import cmd_encrypt, cmd_decrypt, cmd_rekey
|
|
10
|
+
from tokenade.cli.proxy import cmd_proxy
|
|
11
|
+
from tokenade.cli.management import (
|
|
12
|
+
cmd_sessions, cmd_health, cmd_refresh, cmd_share, cmd_unshare,
|
|
13
|
+
cmd_sync, cmd_monitor, cmd_analytics,
|
|
14
|
+
cmd_refresh_oauth, cmd_oauth_config, cmd_batch_refresh, cmd_cicd, cmd_ci,
|
|
15
|
+
cmd_validate_session, cmd_encrypted_refresh, cmd_launch,
|
|
16
|
+
cmd_refresh_browser, cmd_accounts, cmd_patch_chrome,
|
|
17
|
+
cmd_daemon, cmd_versions, cmd_rollback, cmd_session_diff,
|
|
18
|
+
cmd_logs, cmd_health_report, cmd_mobile_import, cmd_clone_profile, cmd_import,
|
|
19
|
+
cmd_container, cmd_k8s, cmd_fleet, cmd_autopsy, cmd_cloak, cmd_tui,
|
|
20
|
+
)
|
|
21
|
+
from tokenade.cli.advanced import (
|
|
22
|
+
cmd_batch_export, cmd_batch_load, cmd_validate, cmd_validate_rules,
|
|
23
|
+
cmd_diff, cmd_fingerprint, cmd_test, cmd_setup,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_config(args):
|
|
28
|
+
"""Manage configuration (~/.tokenade/config.json)."""
|
|
29
|
+
from tokenade.core.config import load_config, DEFAULTS
|
|
30
|
+
|
|
31
|
+
config = load_config()
|
|
32
|
+
|
|
33
|
+
if args.config_command == "path":
|
|
34
|
+
print(config.config_path)
|
|
35
|
+
|
|
36
|
+
elif args.config_command == "show":
|
|
37
|
+
print(f"\n📋 Tokenade Config ({config.config_path})\n")
|
|
38
|
+
for key in sorted(DEFAULTS.keys()):
|
|
39
|
+
value = config.get(key)
|
|
40
|
+
default = DEFAULTS[key]
|
|
41
|
+
marker = "" if value != default else " (default)"
|
|
42
|
+
print(f" {key}: {value}{marker}")
|
|
43
|
+
|
|
44
|
+
elif args.config_command == "get":
|
|
45
|
+
if not args.key:
|
|
46
|
+
print("❌ Usage: tokenade config get <key>")
|
|
47
|
+
return
|
|
48
|
+
value = config.get(args.key)
|
|
49
|
+
if value is None:
|
|
50
|
+
print(f"❌ Unknown config key: {args.key}")
|
|
51
|
+
else:
|
|
52
|
+
print(value)
|
|
53
|
+
|
|
54
|
+
elif args.config_command == "set":
|
|
55
|
+
if not args.key or not args.value:
|
|
56
|
+
print("❌ Usage: tokenade config set <key> <value>")
|
|
57
|
+
return
|
|
58
|
+
# Type coercion for booleans
|
|
59
|
+
value = args.value
|
|
60
|
+
if value.lower() in ("true", "false"):
|
|
61
|
+
value = value.lower() == "true"
|
|
62
|
+
elif value.isdigit():
|
|
63
|
+
value = int(value)
|
|
64
|
+
config.set(args.key, value)
|
|
65
|
+
config.save()
|
|
66
|
+
print(f"✅ Set {args.key} = {value}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cmd_completion(args):
|
|
70
|
+
"""Generate shell completion scripts."""
|
|
71
|
+
shell = args.shell
|
|
72
|
+
|
|
73
|
+
if shell == "bash":
|
|
74
|
+
print('''# Tokenade bash completion
|
|
75
|
+
_tokenade() {
|
|
76
|
+
local cur prev commands
|
|
77
|
+
COMPREPLY=()
|
|
78
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
79
|
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
80
|
+
commands="setup config extract transfer test fingerprint validate export load inject-profile encrypt decrypt rekey batch-export batch-load health refresh proxy sessions share unshare sync validate-rules diff plugin completion launch refresh-browser accounts patch-chrome"
|
|
81
|
+
|
|
82
|
+
if [[ ${cur} == -* ]] ; then
|
|
83
|
+
COMPREPLY=( $(compgen -W "--help --version --verbose" -- ${cur}) )
|
|
84
|
+
return 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) )
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
complete -F _tokenade tokenade
|
|
91
|
+
''')
|
|
92
|
+
elif shell == "zsh":
|
|
93
|
+
print('''# Tokenade zsh completion
|
|
94
|
+
_tokenade() {
|
|
95
|
+
local commands
|
|
96
|
+
commands=(
|
|
97
|
+
'setup:Setup accounts'
|
|
98
|
+
'config:Manage configuration'
|
|
99
|
+
'extract:Extract tokens'
|
|
100
|
+
'export:Export session from browser'
|
|
101
|
+
'load:Load session file'
|
|
102
|
+
'inject-profile:Inject cookies into profile'
|
|
103
|
+
'proxy:Start proxy server'
|
|
104
|
+
'health:Check session health'
|
|
105
|
+
'refresh:Refresh session'
|
|
106
|
+
'encrypt:Encrypt session file'
|
|
107
|
+
'decrypt:Decrypt session file'
|
|
108
|
+
'sessions:Manage sessions'
|
|
109
|
+
'share:Share session'
|
|
110
|
+
'batch-export:Batch export'
|
|
111
|
+
'batch-load:Batch load'
|
|
112
|
+
'diff:Compare sessions'
|
|
113
|
+
'completion:Generate shell completion'
|
|
114
|
+
)
|
|
115
|
+
_describe 'tokenade' commands
|
|
116
|
+
}
|
|
117
|
+
compdef _tokenade tokenade
|
|
118
|
+
''')
|
|
119
|
+
elif shell == "fish":
|
|
120
|
+
print('''# Tokenade fish completion
|
|
121
|
+
complete -c tokenade -f
|
|
122
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "setup" -d "Setup accounts"
|
|
123
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "config" -d "Manage configuration"
|
|
124
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "extract" -d "Extract tokens"
|
|
125
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "export" -d "Export session"
|
|
126
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "load" -d "Load session"
|
|
127
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "proxy" -d "Start proxy"
|
|
128
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "health" -d "Check health"
|
|
129
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "encrypt" -d "Encrypt session"
|
|
130
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "decrypt" -d "Decrypt session"
|
|
131
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "sessions" -d "Manage sessions"
|
|
132
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "share" -d "Share session"
|
|
133
|
+
complete -c tokenade -n "__fish_use_subcommand" -a "completion" -d "Shell completion"
|
|
134
|
+
''')
|
|
135
|
+
else:
|
|
136
|
+
print(f"Unsupported shell: {shell}. Use bash, zsh, or fish.")
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def cmd_plugin(args):
|
|
141
|
+
"""Manage plugins."""
|
|
142
|
+
from tokenade.core.integration.plugin_registry import PluginRegistry
|
|
143
|
+
from tokenade.core.integration.plugin_loader import PluginLoader
|
|
144
|
+
|
|
145
|
+
registry = PluginRegistry()
|
|
146
|
+
loader = PluginLoader()
|
|
147
|
+
|
|
148
|
+
if args.plugin_command == "list":
|
|
149
|
+
if args.available:
|
|
150
|
+
installed_names = {p["name"] for p in loader.discover()}
|
|
151
|
+
print("\n🌐 Available plugins from registry:")
|
|
152
|
+
plugins = registry.search()
|
|
153
|
+
if not plugins:
|
|
154
|
+
print(" No plugins found in registry")
|
|
155
|
+
for p in plugins:
|
|
156
|
+
status = " ✓ installed" if p["name"] in installed_names else ""
|
|
157
|
+
print(f" • {p['name']} v{p.get('version', '?')} — {p.get('description', '')}{status}")
|
|
158
|
+
else:
|
|
159
|
+
print("\n📦 Installed plugins:")
|
|
160
|
+
installed = loader.discover()
|
|
161
|
+
if not installed:
|
|
162
|
+
print(" No plugins installed. Use 'tokenade plugin install <name>' to install.")
|
|
163
|
+
for p in installed:
|
|
164
|
+
enabled = " ✓" if p.get("enabled", True) else " (disabled)"
|
|
165
|
+
print(f" • {p['name']} v{p.get('version', '?')} ({p.get('type', '?')}){enabled} — {p.get('description', '')}")
|
|
166
|
+
|
|
167
|
+
elif args.plugin_command == "install":
|
|
168
|
+
print(f"\n📥 Installing plugin: {args.name}")
|
|
169
|
+
if registry.install(args.name):
|
|
170
|
+
from tokenade.core.integration.plugin_verifier import PluginVerifier
|
|
171
|
+
verifier = PluginVerifier()
|
|
172
|
+
verifier.register_plugin(args.name)
|
|
173
|
+
installed = loader.discover()
|
|
174
|
+
deps_installed = [p["name"] for p in installed if p["name"] != args.name]
|
|
175
|
+
if deps_installed:
|
|
176
|
+
print(f" 📦 Dependencies installed: {', '.join(deps_installed)}")
|
|
177
|
+
print(" ✅ Plugin installed successfully")
|
|
178
|
+
else:
|
|
179
|
+
print(" ❌ Failed to install plugin")
|
|
180
|
+
|
|
181
|
+
elif args.plugin_command == "uninstall":
|
|
182
|
+
print(f"\n🗑️ Uninstalling plugin: {args.name}")
|
|
183
|
+
if registry.uninstall(args.name):
|
|
184
|
+
print(" ✅ Plugin uninstalled successfully")
|
|
185
|
+
else:
|
|
186
|
+
print(" ❌ Failed to uninstall plugin (not installed or dependency conflict)")
|
|
187
|
+
|
|
188
|
+
elif args.plugin_command == "info":
|
|
189
|
+
installed = loader.discover()
|
|
190
|
+
plugin = None
|
|
191
|
+
for p in installed:
|
|
192
|
+
if p["name"] == args.name:
|
|
193
|
+
plugin = p
|
|
194
|
+
break
|
|
195
|
+
if not plugin:
|
|
196
|
+
registry_details = registry.get_plugin_details(args.name)
|
|
197
|
+
if registry_details:
|
|
198
|
+
print(f"\n📋 Plugin: {args.name} (not installed)")
|
|
199
|
+
print(f" Version: {registry_details.get('version', '?')}")
|
|
200
|
+
print(f" Type: {registry_details.get('type', '?')}")
|
|
201
|
+
print(f" Author: {registry_details.get('author', '?')}")
|
|
202
|
+
print(f" Description: {registry_details.get('description', '')}")
|
|
203
|
+
if registry_details.get("dependencies"):
|
|
204
|
+
print(f" Dependencies: {', '.join(registry_details['dependencies'])}")
|
|
205
|
+
print(f" Install: tokenade plugin install {args.name}")
|
|
206
|
+
else:
|
|
207
|
+
print(f"❌ Plugin not found: {args.name}")
|
|
208
|
+
return
|
|
209
|
+
installed_ver = plugin.get("version", "?")
|
|
210
|
+
enabled = plugin.get("enabled", True)
|
|
211
|
+
print(f"\n📋 Plugin: {plugin['name']}")
|
|
212
|
+
print(f" Version: {installed_ver}")
|
|
213
|
+
print(f" Type: {plugin.get('type', '?')}")
|
|
214
|
+
print(f" Author: {plugin.get('author', '?')}")
|
|
215
|
+
print(f" Description: {plugin.get('description', '')}")
|
|
216
|
+
print(f" Status: {'enabled' if enabled else 'disabled'}")
|
|
217
|
+
if plugin.get("dependencies"):
|
|
218
|
+
print(f" Dependencies: {', '.join(plugin['dependencies'])}")
|
|
219
|
+
registry_details = registry.get_plugin_details(args.name)
|
|
220
|
+
if registry_details and registry_details.get("version") != installed_ver:
|
|
221
|
+
print(f" Registry version: {registry_details['version']} (update available)")
|
|
222
|
+
from tokenade.core.integration.plugin_verifier import PluginVerifier
|
|
223
|
+
verifier = PluginVerifier()
|
|
224
|
+
if verifier._local_checksums.get(args.name):
|
|
225
|
+
result = verifier.verify(args.name)
|
|
226
|
+
print(f" Integrity: {'✓ verified' if result.verified else '✗ tampered'}")
|
|
227
|
+
else:
|
|
228
|
+
print(f" Integrity: unregistered (run 'tokenade plugin verify' to register)")
|
|
229
|
+
|
|
230
|
+
elif args.plugin_command == "enable":
|
|
231
|
+
if loader.enable(args.name):
|
|
232
|
+
print(f"✅ Plugin enabled: {args.name}")
|
|
233
|
+
else:
|
|
234
|
+
print(f"❌ Plugin not found: {args.name}")
|
|
235
|
+
|
|
236
|
+
elif args.plugin_command == "disable":
|
|
237
|
+
if loader.disable(args.name):
|
|
238
|
+
print(f"✅ Plugin disabled: {args.name}")
|
|
239
|
+
else:
|
|
240
|
+
print(f"❌ Plugin not found: {args.name}")
|
|
241
|
+
|
|
242
|
+
elif args.plugin_command == "update":
|
|
243
|
+
if args.name:
|
|
244
|
+
print(f"\n🔄 Updating {args.name}...")
|
|
245
|
+
if registry.update(args.name):
|
|
246
|
+
print(f" ✅ Updated: {args.name}")
|
|
247
|
+
else:
|
|
248
|
+
print(f" ❌ Failed to update: {args.name}")
|
|
249
|
+
else:
|
|
250
|
+
outdated = registry.get_outdated()
|
|
251
|
+
if not outdated:
|
|
252
|
+
print("\n✅ All plugins are up to date.")
|
|
253
|
+
else:
|
|
254
|
+
print(f"\n🔄 Updating {len(outdated)} plugin(s)...")
|
|
255
|
+
results = registry.update()
|
|
256
|
+
updated = sum(1 for v in results.values() if v)
|
|
257
|
+
failed = len(results) - updated
|
|
258
|
+
print(f" ✅ {updated} updated, ❌ {failed} failed")
|
|
259
|
+
for name, success in results.items():
|
|
260
|
+
if not success:
|
|
261
|
+
print(f" Failed: {name}")
|
|
262
|
+
|
|
263
|
+
elif args.plugin_command == "sync":
|
|
264
|
+
print("\n🔄 Syncing plugins from registry...")
|
|
265
|
+
plugins = registry.get_popular(limit=100)
|
|
266
|
+
installed = {p.name for p in loader.list_all()}
|
|
267
|
+
to_install = [p for p in plugins if p.get("name") not in installed]
|
|
268
|
+
|
|
269
|
+
if not to_install:
|
|
270
|
+
print("✅ All available plugins already installed.")
|
|
271
|
+
else:
|
|
272
|
+
print(f" Installing {len(to_install)} plugin(s)...")
|
|
273
|
+
for p in to_install:
|
|
274
|
+
name = p.get("name", "")
|
|
275
|
+
success = registry.install(name)
|
|
276
|
+
if success:
|
|
277
|
+
print(f" ✅ {name}")
|
|
278
|
+
else:
|
|
279
|
+
print(f" ❌ {name}")
|
|
280
|
+
loader.load_all()
|
|
281
|
+
print(f"\n Done. {len(loader.list_all())} plugins installed.")
|
|
282
|
+
|
|
283
|
+
elif args.plugin_command == "reload":
|
|
284
|
+
loaded = loader.reload(args.name)
|
|
285
|
+
if loaded:
|
|
286
|
+
print(f"✅ Plugin reloaded: {args.name} v{loaded.version}")
|
|
287
|
+
else:
|
|
288
|
+
print(f"❌ Failed to reload: {args.name}")
|
|
289
|
+
|
|
290
|
+
elif args.plugin_command == "search":
|
|
291
|
+
_plugin_search(registry, args)
|
|
292
|
+
|
|
293
|
+
elif args.plugin_command == "categories":
|
|
294
|
+
_plugin_categories(registry)
|
|
295
|
+
|
|
296
|
+
elif args.plugin_command == "popular":
|
|
297
|
+
_plugin_popular(registry, args)
|
|
298
|
+
|
|
299
|
+
elif args.plugin_command == "recent":
|
|
300
|
+
_plugin_recent(registry, args)
|
|
301
|
+
|
|
302
|
+
elif args.plugin_command == "rate":
|
|
303
|
+
_plugin_rate(registry, args)
|
|
304
|
+
|
|
305
|
+
elif args.plugin_command == "ratings":
|
|
306
|
+
_plugin_ratings(registry, args)
|
|
307
|
+
|
|
308
|
+
elif args.plugin_command == "verify":
|
|
309
|
+
_plugin_verify(args)
|
|
310
|
+
|
|
311
|
+
elif args.plugin_command == "outdated":
|
|
312
|
+
_plugin_outdated(registry)
|
|
313
|
+
|
|
314
|
+
elif args.plugin_command == "browse":
|
|
315
|
+
_plugin_browse(registry, args)
|
|
316
|
+
|
|
317
|
+
elif args.plugin_command == "test":
|
|
318
|
+
_plugin_test(args)
|
|
319
|
+
|
|
320
|
+
else:
|
|
321
|
+
print("Usage: tokenade plugin {list|install|uninstall|info|enable|disable|update|reload|search|categories|popular|recent|rate|verify|outdated|browse|test}")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _plugin_search(registry, args):
|
|
325
|
+
"""Search plugins in marketplace."""
|
|
326
|
+
tags = None
|
|
327
|
+
if args.tags:
|
|
328
|
+
tags = [t.strip() for t in args.tags.split(",")]
|
|
329
|
+
|
|
330
|
+
results = registry.search(
|
|
331
|
+
query=args.query or "",
|
|
332
|
+
plugin_type=args.plugin_type or "",
|
|
333
|
+
category=args.category or "",
|
|
334
|
+
tags=tags,
|
|
335
|
+
sort_by=args.sort,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if not results:
|
|
339
|
+
print("No plugins found matching your search.")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
print(f"\n{'=' * 70}")
|
|
343
|
+
print(f"TOKENADE - Plugin Search ({len(results)} results)")
|
|
344
|
+
print(f"{'=' * 70}")
|
|
345
|
+
|
|
346
|
+
for p in results:
|
|
347
|
+
stars = f"★ {p.get('rating', 0):.1f}" if p.get('rating') else ""
|
|
348
|
+
verified = " ✓" if p.get('verified') else ""
|
|
349
|
+
downloads = f"↓ {p.get('downloads', 0)}" if p.get('downloads') else ""
|
|
350
|
+
print(f"\n {p['name']}{verified} v{p.get('version', '?')}")
|
|
351
|
+
print(f" {p.get('description', '')}")
|
|
352
|
+
meta = " | ".join(filter(None, [stars, downloads, f"by {p.get('author', '')}"]))
|
|
353
|
+
if meta:
|
|
354
|
+
print(f" {meta}")
|
|
355
|
+
if p.get('tags'):
|
|
356
|
+
print(f" Tags: {', '.join(p['tags'])}")
|
|
357
|
+
|
|
358
|
+
print(f"\n{'=' * 70}\n")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _plugin_categories(registry):
|
|
362
|
+
"""List plugin categories."""
|
|
363
|
+
categories = registry.get_categories()
|
|
364
|
+
|
|
365
|
+
print(f"\n{'=' * 60}")
|
|
366
|
+
print("TOKENADE - Plugin Categories")
|
|
367
|
+
print(f"{'=' * 60}")
|
|
368
|
+
|
|
369
|
+
for cat in categories:
|
|
370
|
+
print(f"\n {cat.get('icon', '')} {cat['name']}")
|
|
371
|
+
print(f" {cat.get('description', '')}")
|
|
372
|
+
print(f" {cat.get('plugin_count', 0)} plugin(s)")
|
|
373
|
+
|
|
374
|
+
print(f"\n{'=' * 60}\n")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _plugin_popular(registry, args):
|
|
378
|
+
"""Show popular plugins."""
|
|
379
|
+
plugins = registry.get_popular(limit=args.limit)
|
|
380
|
+
|
|
381
|
+
if not plugins:
|
|
382
|
+
print("No plugins found in registry.")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
print(f"\n{'=' * 60}")
|
|
386
|
+
print(f"TOKENADE - Popular Plugins (top {len(plugins)})")
|
|
387
|
+
print(f"{'=' * 60}")
|
|
388
|
+
|
|
389
|
+
for i, p in enumerate(plugins, 1):
|
|
390
|
+
print(f"\n {i}. {p['name']} v{p.get('version', '?')}")
|
|
391
|
+
print(f" {p.get('description', '')}")
|
|
392
|
+
dl = p.get("downloads")
|
|
393
|
+
rt = p.get("rating")
|
|
394
|
+
if dl is None and rt is None:
|
|
395
|
+
print(" metrics: n/a (downloads/ratings not tracked yet)")
|
|
396
|
+
else:
|
|
397
|
+
dl_s = f"↓ {dl} downloads" if dl is not None else "↓ n/a"
|
|
398
|
+
rt_s = f"★ {rt:.1f}" if rt is not None else "★ n/a"
|
|
399
|
+
print(f" {dl_s} | {rt_s}")
|
|
400
|
+
|
|
401
|
+
print(f"\n{'=' * 60}\n")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _plugin_recent(registry, args):
|
|
405
|
+
"""Show recent plugins."""
|
|
406
|
+
plugins = registry.get_recent(limit=args.limit)
|
|
407
|
+
|
|
408
|
+
if not plugins:
|
|
409
|
+
print("No plugins found in registry.")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
print(f"\n{'=' * 60}")
|
|
413
|
+
print(f"TOKENADE - Recent Plugins ({len(plugins)} newest)")
|
|
414
|
+
print(f"{'=' * 60}")
|
|
415
|
+
|
|
416
|
+
for i, p in enumerate(plugins, 1):
|
|
417
|
+
print(f"\n {i}. {p['name']} v{p.get('version', '?')}")
|
|
418
|
+
print(f" {p.get('description', '')}")
|
|
419
|
+
print(f" by {p.get('author', 'unknown')}")
|
|
420
|
+
|
|
421
|
+
print(f"\n{'=' * 60}\n")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _plugin_rate(registry, args):
|
|
425
|
+
"""Rate a plugin."""
|
|
426
|
+
if registry.rate_plugin(args.name, args.rating, args.review or ""):
|
|
427
|
+
print(f"✅ Rated {args.name}: {args.rating}/5")
|
|
428
|
+
if args.review:
|
|
429
|
+
print(f" Review: {args.review}")
|
|
430
|
+
|
|
431
|
+
# Sync to GitHub if PAT is set
|
|
432
|
+
from tokenade.core.integration.rating_sync import RatingSync
|
|
433
|
+
sync = RatingSync()
|
|
434
|
+
if sync.has_pat():
|
|
435
|
+
if sync.submit_rating(args.name, args.rating, args.review or ""):
|
|
436
|
+
print(" 🌐 Synced to GitHub")
|
|
437
|
+
else:
|
|
438
|
+
print(" ⚠️ GitHub sync failed (local rating saved)")
|
|
439
|
+
else:
|
|
440
|
+
print(" 💡 Set github-token to sync globally: tokenade config set github-token <PAT>")
|
|
441
|
+
else:
|
|
442
|
+
print(f"❌ Failed to rate {args.name} (rating must be 1.0-5.0)")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _plugin_ratings(registry, args):
|
|
446
|
+
"""View global ratings from GitHub."""
|
|
447
|
+
from tokenade.core.integration.rating_sync import RatingSync
|
|
448
|
+
sync = RatingSync()
|
|
449
|
+
|
|
450
|
+
global_ratings = sync.get_global_ratings()
|
|
451
|
+
|
|
452
|
+
if not global_ratings:
|
|
453
|
+
print("\n No global ratings found.")
|
|
454
|
+
if not sync.has_pat():
|
|
455
|
+
print(" 💡 Set github-token to sync ratings: tokenade config set github-token <PAT>")
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
name = getattr(args, "name", None)
|
|
459
|
+
|
|
460
|
+
if name:
|
|
461
|
+
# Show specific plugin
|
|
462
|
+
if name in global_ratings:
|
|
463
|
+
r = global_ratings[name]
|
|
464
|
+
stars = "★" * int(r.get("rating", 0)) + "☆" * (5 - int(r.get("rating", 0)))
|
|
465
|
+
print(f"\n {name} — {stars} ({r.get('rating', 0):.1f}/5)")
|
|
466
|
+
print(f" Reviews: {r.get('review_count', 0)}")
|
|
467
|
+
for rev in r.get("reviews", []):
|
|
468
|
+
print(f" ⭐ {rev.get('rating', 0)}: {rev.get('review', '')[:80]}")
|
|
469
|
+
else:
|
|
470
|
+
print(f"\n No global ratings for: {name}")
|
|
471
|
+
else:
|
|
472
|
+
# Show all
|
|
473
|
+
print(f"\n 🌐 Global Ratings ({len(global_ratings)} plugins)\n")
|
|
474
|
+
for name, r in sorted(global_ratings.items()):
|
|
475
|
+
stars = "★" * int(r.get("rating", 0)) + "☆" * (5 - int(r.get("rating", 0)))
|
|
476
|
+
print(f" {stars} {r.get('rating', 0):.1f} {name} ({r.get('review_count', 0)} reviews)")
|
|
477
|
+
if not sync.has_pat():
|
|
478
|
+
print(f"\n 💡 Set github-token to sync: tokenade config set github-token <PAT>")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _plugin_verify(args):
|
|
482
|
+
"""Verify plugin checksums. Auto-registers on first run."""
|
|
483
|
+
from tokenade.core.integration.plugin_verifier import PluginVerifier
|
|
484
|
+
|
|
485
|
+
verifier = PluginVerifier()
|
|
486
|
+
plugin_names = [args.name] if args.name else [
|
|
487
|
+
d.name for d in sorted(verifier.plugins_dir.iterdir())
|
|
488
|
+
if d.is_dir() and not d.name.startswith(".") and (d / "plugin.json").exists()
|
|
489
|
+
]
|
|
490
|
+
|
|
491
|
+
if not plugin_names:
|
|
492
|
+
print("No installed plugins to verify.")
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
results = []
|
|
496
|
+
registered = []
|
|
497
|
+
for name in plugin_names:
|
|
498
|
+
if not verifier._local_checksums.get(name):
|
|
499
|
+
verifier.register_plugin(name)
|
|
500
|
+
registered.append(name)
|
|
501
|
+
results.append(verifier.verify(name))
|
|
502
|
+
else:
|
|
503
|
+
results.append(verifier.verify(name))
|
|
504
|
+
|
|
505
|
+
print(f"\n{'=' * 60}")
|
|
506
|
+
print("TOKENADE - Plugin Verification")
|
|
507
|
+
print(f"{'=' * 60}")
|
|
508
|
+
|
|
509
|
+
all_ok = True
|
|
510
|
+
for r in results:
|
|
511
|
+
if r.plugin_name in registered:
|
|
512
|
+
icon = "📝"
|
|
513
|
+
status = f"registered ({r.files_checked} files checksummed)"
|
|
514
|
+
elif r.verified:
|
|
515
|
+
icon = "✅"
|
|
516
|
+
status = r.summary
|
|
517
|
+
else:
|
|
518
|
+
icon = "❌"
|
|
519
|
+
status = r.summary
|
|
520
|
+
all_ok = False
|
|
521
|
+
print(f"\n {icon} {r.plugin_name}: {status}")
|
|
522
|
+
for err in r.errors:
|
|
523
|
+
print(f" ⚠️ {err}")
|
|
524
|
+
|
|
525
|
+
print(f"\n{'=' * 60}")
|
|
526
|
+
if all_ok:
|
|
527
|
+
print("All plugins verified successfully.")
|
|
528
|
+
else:
|
|
529
|
+
print("Some plugins failed verification!")
|
|
530
|
+
print(f"{'=' * 60}\n")
|
|
531
|
+
print(f"{'=' * 60}\n")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _plugin_outdated(registry):
|
|
535
|
+
"""Show outdated plugins."""
|
|
536
|
+
outdated = registry.get_outdated()
|
|
537
|
+
|
|
538
|
+
if not outdated:
|
|
539
|
+
print("All installed plugins are up to date.")
|
|
540
|
+
return
|
|
541
|
+
|
|
542
|
+
print(f"\n{'=' * 60}")
|
|
543
|
+
print(f"TOKENADE - Outdated Plugins ({len(outdated)})")
|
|
544
|
+
print(f"{'=' * 60}")
|
|
545
|
+
|
|
546
|
+
for p in outdated:
|
|
547
|
+
print(f"\n {p['name']}")
|
|
548
|
+
print(f" Installed: {p['installed_version']} → Available: {p['available_version']}")
|
|
549
|
+
if p.get('description'):
|
|
550
|
+
print(f" {p['description']}")
|
|
551
|
+
|
|
552
|
+
print(f"\n Run 'tokenade plugin update' to update all.")
|
|
553
|
+
print(f"{'=' * 60}\n")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _plugin_browse(registry, args):
|
|
557
|
+
"""Generate static HTML marketplace page."""
|
|
558
|
+
from pathlib import Path
|
|
559
|
+
from tokenade.core.integration.plugin_browser import generate_marketplace_html
|
|
560
|
+
|
|
561
|
+
plugins = registry.search()
|
|
562
|
+
categories = registry.get_categories()
|
|
563
|
+
|
|
564
|
+
output_path = args.output or str(Path.home() / ".tokenade" / "marketplace.html")
|
|
565
|
+
|
|
566
|
+
path = generate_marketplace_html(plugins, categories, output_path)
|
|
567
|
+
print(f"✅ Marketplace page generated: {path}")
|
|
568
|
+
print(f" Open in browser: file://{path}")
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _plugin_test(args):
|
|
572
|
+
"""Run tests on installed plugins."""
|
|
573
|
+
from tokenade.core.integration.plugin_testing import PluginTestRunner
|
|
574
|
+
|
|
575
|
+
runner = PluginTestRunner()
|
|
576
|
+
|
|
577
|
+
if args.name:
|
|
578
|
+
suites = [runner.test_plugin(args.name)]
|
|
579
|
+
else:
|
|
580
|
+
suites = runner.test_all()
|
|
581
|
+
|
|
582
|
+
if not suites:
|
|
583
|
+
print("No plugins found to test.")
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
print(f"\n{'=' * 60}")
|
|
587
|
+
print("TOKENADE - Plugin Tests")
|
|
588
|
+
print(f"{'=' * 60}")
|
|
589
|
+
|
|
590
|
+
total_passed = 0
|
|
591
|
+
total_failed = 0
|
|
592
|
+
|
|
593
|
+
for suite in suites:
|
|
594
|
+
status = "✅" if suite.passed else "❌"
|
|
595
|
+
print(f"\n{status} {suite.summary()}")
|
|
596
|
+
for result in suite.results:
|
|
597
|
+
icon = " ✓" if result.passed else " ✗"
|
|
598
|
+
msg = f" — {result.message}" if result.message and not result.passed else ""
|
|
599
|
+
print(f"{icon} {result.test_name}{msg}")
|
|
600
|
+
total_passed += suite.passed_count
|
|
601
|
+
total_failed += suite.failed_count
|
|
602
|
+
|
|
603
|
+
print(f"\n{'=' * 60}")
|
|
604
|
+
print(f"Results: {total_passed} passed, {total_failed} failed")
|
|
605
|
+
print(f"{'=' * 60}\n")
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def cmd_profile(args):
|
|
609
|
+
"""Manage browser profiles."""
|
|
610
|
+
from tokenade.core.browser.profiles import ProfileManager
|
|
611
|
+
|
|
612
|
+
manager = ProfileManager()
|
|
613
|
+
|
|
614
|
+
if args.profile_command == "create":
|
|
615
|
+
proxy = None
|
|
616
|
+
if args.proxy:
|
|
617
|
+
proxy = {"url": args.proxy}
|
|
618
|
+
tags = [t.strip() for t in args.tags.split(",")] if args.tags else []
|
|
619
|
+
try:
|
|
620
|
+
profile = manager.create_profile(
|
|
621
|
+
name=args.name,
|
|
622
|
+
browser=args.browser,
|
|
623
|
+
os_name=args.os_name,
|
|
624
|
+
proxy=proxy,
|
|
625
|
+
tags=tags,
|
|
626
|
+
notes=args.notes or "",
|
|
627
|
+
)
|
|
628
|
+
print(f"\n✅ Created profile: {profile.name}")
|
|
629
|
+
print(f" Browser: {profile.browser} | OS: {profile.os}")
|
|
630
|
+
print(f" ID: {profile.id}")
|
|
631
|
+
if profile.fingerprint.get("navigator"):
|
|
632
|
+
nav = profile.fingerprint["navigator"]
|
|
633
|
+
print(f" Hardware: {nav.get('hardwareConcurrency', '?')} cores, {nav.get('deviceMemory', '?')} GB RAM")
|
|
634
|
+
if profile.fingerprint.get("webgl"):
|
|
635
|
+
gl = profile.fingerprint["webgl"]
|
|
636
|
+
print(f" GPU: {gl.get('renderer', '?')}")
|
|
637
|
+
print()
|
|
638
|
+
except ValueError as e:
|
|
639
|
+
print(f"\n❌ {e}\n")
|
|
640
|
+
|
|
641
|
+
elif args.profile_command == "list":
|
|
642
|
+
profiles = manager.list_profiles(browser=args.browser, tag=args.tag)
|
|
643
|
+
if not profiles:
|
|
644
|
+
print("\nNo profiles found.\n")
|
|
645
|
+
return
|
|
646
|
+
print(f"\n{'=' * 60}")
|
|
647
|
+
print(f"TOKENADE - Browser Profiles ({len(profiles)} total)")
|
|
648
|
+
print(f"{'=' * 60}")
|
|
649
|
+
for p in profiles:
|
|
650
|
+
last_used = time.strftime("%Y-%m-%d %H:%M", time.localtime(p.last_used)) if p.last_used else "never"
|
|
651
|
+
tags = f" [{', '.join(p.tags)}]" if p.tags else ""
|
|
652
|
+
print(f"\n {p.name}{tags}")
|
|
653
|
+
print(f" {p.browser} / {p.os} | ID: {p.id}")
|
|
654
|
+
print(f" Last used: {last_used}")
|
|
655
|
+
if p.proxy:
|
|
656
|
+
print(f" Proxy: {p.proxy.get('url', 'configured')}")
|
|
657
|
+
print(f"\n{'=' * 60}\n")
|
|
658
|
+
|
|
659
|
+
elif args.profile_command == "get":
|
|
660
|
+
profile = manager.get_profile(args.name)
|
|
661
|
+
if not profile:
|
|
662
|
+
print(f"\n❌ Profile not found: {args.name}\n")
|
|
663
|
+
return
|
|
664
|
+
print(f"\n{'=' * 60}")
|
|
665
|
+
print(f"TOKENADE - Profile: {profile.name}")
|
|
666
|
+
print(f"{'=' * 60}")
|
|
667
|
+
print(f" ID: {profile.id}")
|
|
668
|
+
print(f" Browser: {profile.browser}")
|
|
669
|
+
print(f" OS: {profile.os}")
|
|
670
|
+
print(f" Created: {time.strftime('%Y-%m-%d %H:%M', time.localtime(profile.created_at))}")
|
|
671
|
+
print(f" Updated: {time.strftime('%Y-%m-%d %H:%M', time.localtime(profile.updated_at))}")
|
|
672
|
+
last_used = time.strftime('%Y-%m-%d %H:%M', time.localtime(profile.last_used)) if profile.last_used else "never"
|
|
673
|
+
print(f" Last used: {last_used}")
|
|
674
|
+
if profile.tags:
|
|
675
|
+
print(f" Tags: {', '.join(profile.tags)}")
|
|
676
|
+
if profile.notes:
|
|
677
|
+
print(f" Notes: {profile.notes}")
|
|
678
|
+
if profile.proxy:
|
|
679
|
+
print(f" Proxy: {profile.proxy.get('url', 'configured')}")
|
|
680
|
+
if profile.fingerprint:
|
|
681
|
+
nav = profile.fingerprint.get("navigator", {})
|
|
682
|
+
gl = profile.fingerprint.get("webgl", {})
|
|
683
|
+
scr = profile.fingerprint.get("screen", {})
|
|
684
|
+
print(f"\n Fingerprint:")
|
|
685
|
+
print(f" Platform: {nav.get('platform', '?')}")
|
|
686
|
+
print(f" Language: {nav.get('language', '?')}")
|
|
687
|
+
print(f" Cores: {nav.get('hardwareConcurrency', '?')} | RAM: {nav.get('deviceMemory', '?')} GB")
|
|
688
|
+
print(f" Screen: {scr.get('width', '?')}x{scr.get('height', '?')}")
|
|
689
|
+
print(f" GPU: {gl.get('renderer', '?')}")
|
|
690
|
+
print(f"\n{'=' * 60}\n")
|
|
691
|
+
|
|
692
|
+
elif args.profile_command == "delete":
|
|
693
|
+
if manager.delete_profile(args.name):
|
|
694
|
+
print(f"\n✅ Deleted profile: {args.name}\n")
|
|
695
|
+
else:
|
|
696
|
+
print(f"\n❌ Profile not found: {args.name}\n")
|
|
697
|
+
|
|
698
|
+
elif args.profile_command == "export":
|
|
699
|
+
try:
|
|
700
|
+
path = manager.export_profile(args.name, args.output or f"{args.name}.zip")
|
|
701
|
+
print(f"\n✅ Exported profile: {path}\n")
|
|
702
|
+
except FileNotFoundError as e:
|
|
703
|
+
print(f"\n❌ {e}\n")
|
|
704
|
+
|
|
705
|
+
elif args.profile_command == "import":
|
|
706
|
+
try:
|
|
707
|
+
profile = manager.import_profile(args.archive, name=args.name)
|
|
708
|
+
print(f"\n✅ Imported profile: {profile.name}\n")
|
|
709
|
+
except (FileNotFoundError, ValueError) as e:
|
|
710
|
+
print(f"\n❌ {e}\n")
|
|
711
|
+
|
|
712
|
+
elif args.profile_command == "recent":
|
|
713
|
+
profiles = manager.get_recent_profiles(limit=args.limit)
|
|
714
|
+
if not profiles:
|
|
715
|
+
print("\nNo recently used profiles.\n")
|
|
716
|
+
return
|
|
717
|
+
print(f"\nRecently used profiles:")
|
|
718
|
+
for i, p in enumerate(profiles, 1):
|
|
719
|
+
last_used = time.strftime("%Y-%m-%d %H:%M", time.localtime(p.last_used)) if p.last_used else "never"
|
|
720
|
+
print(f" {i}. {p.name} ({p.browser}/{p.os}) — last used: {last_used}")
|
|
721
|
+
print()
|
|
722
|
+
|
|
723
|
+
elif args.profile_command == "stats":
|
|
724
|
+
stats = manager.get_stats()
|
|
725
|
+
print(f"\nProfile Statistics:")
|
|
726
|
+
print(f" Total: {stats['total']}")
|
|
727
|
+
for browser, count in stats.get("by_browser", {}).items():
|
|
728
|
+
print(f" {browser}: {count}")
|
|
729
|
+
print()
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def cmd_stealth(args):
|
|
733
|
+
"""Browser stealth management."""
|
|
734
|
+
from pathlib import Path
|
|
735
|
+
from tokenade.core.browser.stealth import StealthManager
|
|
736
|
+
from tokenade.core.browser.dependencies import DependencyChecker
|
|
737
|
+
|
|
738
|
+
if args.stealth_action == "test":
|
|
739
|
+
from tokenade.core.browser.stealth_test import StealthTestSuite
|
|
740
|
+
from tokenade.core.browser.dashboard import generate_html_report, generate_json_report
|
|
741
|
+
|
|
742
|
+
browser = args.browser
|
|
743
|
+
print(f"\n🔍 Running stealth tests ({browser})...")
|
|
744
|
+
suite = StealthTestSuite(browser=browser, headless=True)
|
|
745
|
+
report = asyncio.run(suite.run_all(url=args.url))
|
|
746
|
+
|
|
747
|
+
print(f"\n{'=' * 60}")
|
|
748
|
+
print(f" STEALTH SCORE: {report.overall_score:.0f}/100 ({report._grade()})")
|
|
749
|
+
print(f"{'=' * 60}")
|
|
750
|
+
print(f" {report.passed} passed, {report.failed} failed, {report.warned} warned")
|
|
751
|
+
print()
|
|
752
|
+
|
|
753
|
+
for r in report.results:
|
|
754
|
+
icon = {"pass": "✅", "fail": "❌", "warn": "⚠️", "skip": "○"}[r.verdict.value]
|
|
755
|
+
print(f" {icon} {r.name} ({r.score:.0f})")
|
|
756
|
+
|
|
757
|
+
# Save reports
|
|
758
|
+
html_path = args.output or str(Path.home() / ".tokenade" / "stealth_report.html")
|
|
759
|
+
generate_html_report(report, html_path)
|
|
760
|
+
json_path = html_path.replace(".html", ".json")
|
|
761
|
+
generate_json_report(report, json_path)
|
|
762
|
+
print(f"\n 📄 HTML report: {html_path}")
|
|
763
|
+
print(f" 📄 JSON report: {json_path}")
|
|
764
|
+
print(f"{'=' * 60}\n")
|
|
765
|
+
|
|
766
|
+
elif args.stealth_action == "report":
|
|
767
|
+
import json
|
|
768
|
+
from pathlib import Path
|
|
769
|
+
print(f"\n📊 Stealth Report")
|
|
770
|
+
manager = StealthManager()
|
|
771
|
+
config = manager.get_config_dict()
|
|
772
|
+
enabled = [k for k, v in config.items() if v is True and k.startswith("enable_")]
|
|
773
|
+
print(f" Enabled patches: {len(enabled)}")
|
|
774
|
+
for patch in enabled:
|
|
775
|
+
print(f" ✓ {patch.replace('enable_', '')}")
|
|
776
|
+
print(f"\n WebGL vendor: {config['webgl_vendor']}")
|
|
777
|
+
print(f" WebGL renderer: {config['webgl_renderer']}")
|
|
778
|
+
print(f" Screen: {config['screen_width']}x{config['screen_height']}")
|
|
779
|
+
output_path = args.output or str(Path.home() / ".tokenade" / "stealth_report.json")
|
|
780
|
+
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
|
781
|
+
with open(output_path, "w") as f:
|
|
782
|
+
json.dump(config, f, indent=2)
|
|
783
|
+
print(f" Report saved: {output_path}")
|
|
784
|
+
|
|
785
|
+
elif args.stealth_action == "deps":
|
|
786
|
+
checker = DependencyChecker()
|
|
787
|
+
report = checker.get_report("chromium")
|
|
788
|
+
print(f"\n📦 System Dependencies ({report['system']})")
|
|
789
|
+
print(f" Package manager: {report['package_manager'] or 'not found'}")
|
|
790
|
+
print(f" Installed: {report['installed']}/{report['total']}")
|
|
791
|
+
if report['missing_packages']:
|
|
792
|
+
print(f" Missing ({len(report['missing_packages'])}):")
|
|
793
|
+
for pkg in report['missing_packages']:
|
|
794
|
+
print(f" ❌ {pkg}")
|
|
795
|
+
else:
|
|
796
|
+
print(f" ✅ All dependencies installed")
|
|
797
|
+
|
|
798
|
+
elif args.stealth_action == "deps-install":
|
|
799
|
+
print(f"\n📦 Installing missing dependencies...")
|
|
800
|
+
checker = DependencyChecker()
|
|
801
|
+
results = checker.install_playwright_deps()
|
|
802
|
+
installed = sum(1 for r in results if r.success)
|
|
803
|
+
failed = sum(1 for r in results if not r.success)
|
|
804
|
+
print(f" Installed: {installed}, Failed: {failed}")
|
|
805
|
+
for r in results:
|
|
806
|
+
if r.success:
|
|
807
|
+
print(f" ✅ {r.name}")
|
|
808
|
+
else:
|
|
809
|
+
print(f" ❌ {r.name}: {r.message}")
|
|
810
|
+
|
|
811
|
+
elif args.stealth_action == "battle":
|
|
812
|
+
from tokenade.core.browser.battle import BattleTestSuite, DETECTION_SITES
|
|
813
|
+
|
|
814
|
+
browser = args.browser
|
|
815
|
+
sites = args.site if args.site else list(DETECTION_SITES.keys())
|
|
816
|
+
timeout_ms = args.timeout * 1000
|
|
817
|
+
|
|
818
|
+
print(f"\n⚔️ Running battle tests ({browser})...")
|
|
819
|
+
print(f" Sites: {len(sites)}")
|
|
820
|
+
for s in sites:
|
|
821
|
+
cfg = DETECTION_SITES.get(s, {})
|
|
822
|
+
print(f" • {cfg.get('name', s)}")
|
|
823
|
+
print()
|
|
824
|
+
|
|
825
|
+
suite = BattleTestSuite(
|
|
826
|
+
browser=browser,
|
|
827
|
+
headless=True,
|
|
828
|
+
sites=sites,
|
|
829
|
+
timeout_ms=timeout_ms,
|
|
830
|
+
)
|
|
831
|
+
report = suite.run_sync()
|
|
832
|
+
|
|
833
|
+
print(report.summary())
|
|
834
|
+
print(f"\n Duration: {report.duration_ms / 1000:.1f}s")
|
|
835
|
+
|
|
836
|
+
if args.output:
|
|
837
|
+
import json as json_mod
|
|
838
|
+
from pathlib import Path
|
|
839
|
+
out = Path(args.output)
|
|
840
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
841
|
+
data = {
|
|
842
|
+
"overall_score": report.overall_score,
|
|
843
|
+
"grade": report.grade,
|
|
844
|
+
"browser": report.browser,
|
|
845
|
+
"timestamp": report.timestamp,
|
|
846
|
+
"duration_ms": report.duration_ms,
|
|
847
|
+
"passed": report.passed,
|
|
848
|
+
"detected": report.detected,
|
|
849
|
+
"partial": report.partial,
|
|
850
|
+
"errors": report.errors,
|
|
851
|
+
"skipped": report.skipped,
|
|
852
|
+
"sites": [
|
|
853
|
+
{
|
|
854
|
+
"name": r.site_name,
|
|
855
|
+
"url": r.url,
|
|
856
|
+
"verdict": r.verdict.value,
|
|
857
|
+
"score": r.score,
|
|
858
|
+
"details": r.detection_details,
|
|
859
|
+
"error": r.error,
|
|
860
|
+
"duration_ms": r.duration_ms,
|
|
861
|
+
}
|
|
862
|
+
for r in report.site_results
|
|
863
|
+
],
|
|
864
|
+
}
|
|
865
|
+
with open(out, "w") as f:
|
|
866
|
+
json_mod.dump(data, f, indent=2)
|
|
867
|
+
print(f"\n 📄 JSON report: {args.output}")
|
|
868
|
+
|
|
869
|
+
print(f"\n{'=' * 60}\n")
|
|
870
|
+
|
|
871
|
+
else:
|
|
872
|
+
print("Usage: tokenade stealth {test|report|battle|deps|deps-install}")
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def cmd_deps(args):
|
|
876
|
+
"""System dependency management."""
|
|
877
|
+
from tokenade.core.browser.dependencies import DependencyChecker
|
|
878
|
+
|
|
879
|
+
checker = DependencyChecker()
|
|
880
|
+
|
|
881
|
+
if args.deps_action == "check":
|
|
882
|
+
browser = getattr(args, "browser", "chromium")
|
|
883
|
+
report = checker.get_report(browser)
|
|
884
|
+
print(f"\n📦 System Dependencies ({report['system']})")
|
|
885
|
+
print(f" Package manager: {report['package_manager'] or 'not found'}")
|
|
886
|
+
print(f" Browser: {browser}")
|
|
887
|
+
print(f" Installed: {report['installed']}/{report['total']}")
|
|
888
|
+
if report['missing_packages']:
|
|
889
|
+
print(f" Missing packages:")
|
|
890
|
+
for pkg in report['missing_packages']:
|
|
891
|
+
print(f" ❌ {pkg}")
|
|
892
|
+
print(f"\n Install: tokenade deps install")
|
|
893
|
+
else:
|
|
894
|
+
print(f" ✅ All dependencies installed")
|
|
895
|
+
|
|
896
|
+
elif args.deps_action == "install":
|
|
897
|
+
browser = getattr(args, "browser", "chromium")
|
|
898
|
+
playwright_only = getattr(args, "playwright", False)
|
|
899
|
+
|
|
900
|
+
if playwright_only:
|
|
901
|
+
print(f"\n📦 Installing Playwright dependencies...")
|
|
902
|
+
results = checker.install_playwright_deps()
|
|
903
|
+
else:
|
|
904
|
+
print(f"\n📦 Installing {browser} dependencies...")
|
|
905
|
+
results = checker.install_missing(browser)
|
|
906
|
+
|
|
907
|
+
installed = sum(1 for r in results if r.success)
|
|
908
|
+
failed = sum(1 for r in results if not r.success)
|
|
909
|
+
print(f" Installed: {installed}, Failed: {failed}")
|
|
910
|
+
for r in results:
|
|
911
|
+
if r.success:
|
|
912
|
+
print(f" ✅ {r.name}")
|
|
913
|
+
else:
|
|
914
|
+
print(f" ❌ {r.name}: {r.message}")
|
|
915
|
+
|
|
916
|
+
else:
|
|
917
|
+
print("Usage: tokenade deps {check|install}")
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def cmd_serve(args):
|
|
921
|
+
"""Start the API server."""
|
|
922
|
+
import asyncio
|
|
923
|
+
from tokenade.core.api.server import TokenadeAPIServer, APIServerConfig
|
|
924
|
+
|
|
925
|
+
config = APIServerConfig(
|
|
926
|
+
host=args.host,
|
|
927
|
+
port=args.port,
|
|
928
|
+
api_key=args.api_key,
|
|
929
|
+
sessions_dir=args.sessions_dir or "~/.tokenade/sessions",
|
|
930
|
+
cors_origins=args.cors.split(",") if args.cors else None,
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
print(f"\n🚀 Starting Tokenade API server...")
|
|
934
|
+
print(f" Host: {config.host}")
|
|
935
|
+
print(f" Port: {config.port}")
|
|
936
|
+
print(f" Auth: {'API key required' if config.api_key else 'no authentication'}")
|
|
937
|
+
print(f" Sessions: {config.sessions_dir}")
|
|
938
|
+
print()
|
|
939
|
+
|
|
940
|
+
server = TokenadeAPIServer(config)
|
|
941
|
+
|
|
942
|
+
try:
|
|
943
|
+
asyncio.run(server.start())
|
|
944
|
+
except KeyboardInterrupt:
|
|
945
|
+
print("\n\n🛑 Server stopped")
|
|
946
|
+
except Exception as e:
|
|
947
|
+
print(f"\n❌ Server error: {e}")
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
logging.basicConfig(
|
|
951
|
+
level=logging.INFO,
|
|
952
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
953
|
+
)
|
|
954
|
+
logger = logging.getLogger("tokenade")
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def setup_logging(verbose: bool = False):
|
|
958
|
+
"""Configure logging level and structured output."""
|
|
959
|
+
from tokenade.core.logging.structured import LogManager
|
|
960
|
+
level = "DEBUG" if verbose else "INFO"
|
|
961
|
+
json_output = getattr(setup_logging, '_json_output', False)
|
|
962
|
+
LogManager.setup(level=level, json_output=json_output)
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _build_parser():
|
|
966
|
+
"""Build and return the CLI argument parser (extracted for testability)."""
|
|
967
|
+
from tokenade import __version__
|
|
968
|
+
parser = argparse.ArgumentParser(
|
|
969
|
+
description="Tokenade - Browser session portability tool",
|
|
970
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
971
|
+
epilog="""
|
|
972
|
+
Quick Start:
|
|
973
|
+
1. Export: tokenade export --browser-name firefox --domains "google.com,accounts.google.com" -o my_session.tokenade
|
|
974
|
+
2. Proxy: tokenade proxy -s my_session.tokenade
|
|
975
|
+
3. Browse: Open http://127.0.0.1:9222 and enter the target URL
|
|
976
|
+
|
|
977
|
+
Commands:
|
|
978
|
+
export Extract cookies from browser to .tokenade file
|
|
979
|
+
proxy Start CDP proxy server with donor session
|
|
980
|
+
load Load .tokenade session into a browser
|
|
981
|
+
inject-profile Inject cookies directly into browser profile
|
|
982
|
+
encrypt Encrypt a .tokenade file
|
|
983
|
+
decrypt Decrypt a .tokenade file
|
|
984
|
+
health Check session health
|
|
985
|
+
monitor Monitor session health in real-time
|
|
986
|
+
batch-export Export multiple sites at once
|
|
987
|
+
""",
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
991
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")
|
|
992
|
+
parser.add_argument("--json", dest="json_output", action="store_true", help="Output in JSON format")
|
|
993
|
+
|
|
994
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
995
|
+
|
|
996
|
+
# Setup
|
|
997
|
+
subparsers.add_parser("setup", help="Setup accounts")
|
|
998
|
+
|
|
999
|
+
# Config
|
|
1000
|
+
config_parser = subparsers.add_parser("config", help="Manage configuration")
|
|
1001
|
+
config_parser.add_argument("config_command", choices=["show", "set", "get", "path"],
|
|
1002
|
+
help="Config action")
|
|
1003
|
+
config_parser.add_argument("key", nargs="?", help="Config key")
|
|
1004
|
+
config_parser.add_argument("value", nargs="?", help="Config value")
|
|
1005
|
+
|
|
1006
|
+
# Extract
|
|
1007
|
+
extract_parser = subparsers.add_parser("extract", help="Extract tokens")
|
|
1008
|
+
extract_parser.add_argument("--visible", action="store_true", help="Show browser window")
|
|
1009
|
+
|
|
1010
|
+
# Transfer
|
|
1011
|
+
transfer_parser = subparsers.add_parser("transfer", help="Transfer session")
|
|
1012
|
+
transfer_parser.add_argument("-s", "--session", required=True, help="Session file path")
|
|
1013
|
+
transfer_parser.add_argument("-", "--fingerprint", help="Target fingerprint name")
|
|
1014
|
+
transfer_parser.add_argument("-p", "--profile-dir", default="browser_data/transfer", help="Profile directory")
|
|
1015
|
+
transfer_parser.add_argument("--visible", action="store_true", help="Show browser window")
|
|
1016
|
+
transfer_parser.add_argument("--stealth-level", choices=["basic", "advanced", "maximum"], default="maximum", help="Stealth injection level")
|
|
1017
|
+
transfer_parser.add_argument("--validate-stealth", action="store_true", help="Validate stealth injection after launch")
|
|
1018
|
+
|
|
1019
|
+
# Test
|
|
1020
|
+
test_parser = subparsers.add_parser("test", help="Test portability")
|
|
1021
|
+
test_parser.add_argument("-s", "--session", required=True, help="Session file path")
|
|
1022
|
+
test_parser.add_argument("--source-fp", default="default", help="Source fingerprint")
|
|
1023
|
+
test_parser.add_argument("--target-fp", default="default", help="Target fingerprint")
|
|
1024
|
+
test_parser.add_argument("--variations", action="store_true", help="Test fingerprint variations")
|
|
1025
|
+
test_parser.add_argument("--test-api", action="store_true", help="Test API calls")
|
|
1026
|
+
test_parser.add_argument("--stealth-level", choices=["basic", "advanced", "maximum"], default="maximum", help="Stealth injection level")
|
|
1027
|
+
test_parser.add_argument("--validate-stealth", action="store_true", help="Validate stealth injection")
|
|
1028
|
+
test_parser.add_argument("-o", "--output", help="Output report path")
|
|
1029
|
+
|
|
1030
|
+
# Fingerprint
|
|
1031
|
+
fp_parser = subparsers.add_parser("fingerprint", help="Manage fingerprints")
|
|
1032
|
+
fp_parser.add_argument("action", choices=["list", "collect", "show", "delete"], help="Action")
|
|
1033
|
+
fp_parser.add_argument("-n", "--name", help="Fingerprint name")
|
|
1034
|
+
fp_parser.add_argument("-p", "--profile-dir", help="Browser profile directory")
|
|
1035
|
+
|
|
1036
|
+
# Validate
|
|
1037
|
+
validate_parser = subparsers.add_parser("validate", help="Validate sessions")
|
|
1038
|
+
validate_parser.add_argument("-d", "--sessions-dir", default="sessions", help="Sessions directory")
|
|
1039
|
+
|
|
1040
|
+
# Export
|
|
1041
|
+
export_parser = subparsers.add_parser("export", help="Export session from existing browser")
|
|
1042
|
+
export_parser.add_argument("--browser-name", choices=["chrome", "firefox", "edge", "brave"], help="Browser name")
|
|
1043
|
+
export_parser.add_argument("--browser-path", help="Custom path to browser profile")
|
|
1044
|
+
export_parser.add_argument("--profile", help="Profile name within browser")
|
|
1045
|
+
export_parser.add_argument("--site-config", help="Path to JSON site config file for filtering")
|
|
1046
|
+
export_parser.add_argument("--domains", help="Comma-separated domains to filter (e.g. 'google.com,accounts.google.com')")
|
|
1047
|
+
export_parser.add_argument("--cdp-port", type=int, help="Extract via CDP from running browser (bypasses SQLite decryption)")
|
|
1048
|
+
export_parser.add_argument("--file-path", help="Export from cookies file")
|
|
1049
|
+
export_parser.add_argument("--format", choices=["netscape", "json", "curl"], default="netscape", help="File format")
|
|
1050
|
+
export_parser.add_argument("--collect-fingerprint", action="store_true", help="Collect source browser fingerprint")
|
|
1051
|
+
export_parser.add_argument("--output", "-o", help="Output file path (any extension)")
|
|
1052
|
+
export_parser.add_argument("--list-profiles", action="store_true", help="List available profiles")
|
|
1053
|
+
export_parser.add_argument("--decrypt", action="store_true", help="Decrypt cookies (auto-detected)")
|
|
1054
|
+
export_parser.add_argument("--extract-local-storage", action="store_true", help="Also extract localStorage data")
|
|
1055
|
+
export_parser.add_argument("--local-storage-origin", help="Origin to extract localStorage from")
|
|
1056
|
+
export_parser.add_argument("--full", action="store_true", help="Extract cookies + localStorage + sessionStorage (v3.0 format)")
|
|
1057
|
+
export_parser.add_argument("--no-storage", action="store_true", help="Extract only cookies (backward compat)")
|
|
1058
|
+
export_parser.add_argument("--encrypt-password", help="Encrypt .tokenade file with this password at export time")
|
|
1059
|
+
export_parser.add_argument("--plugin", help="Force specific site handler plugin (e.g., google-handler)")
|
|
1060
|
+
export_parser.add_argument("--no-plugin", action="store_true", help="Skip plugin, use default extraction")
|
|
1061
|
+
export_parser.add_argument("--list-handlers", action="store_true", help="List available site handler plugins")
|
|
1062
|
+
|
|
1063
|
+
# Load
|
|
1064
|
+
load_parser = subparsers.add_parser("load", help="Load session file into browser")
|
|
1065
|
+
load_parser.add_argument("--file", "-", required=True, help="Path to session file")
|
|
1066
|
+
load_parser.add_argument("--site-config", help="Path to JSON site config file for validation")
|
|
1067
|
+
load_parser.add_argument("--fingerprint", help="Target fingerprint name")
|
|
1068
|
+
load_parser.add_argument("--stealth-level", choices=["basic", "advanced", "maximum"], default="maximum", help="Stealth level")
|
|
1069
|
+
load_parser.add_argument("--validate", action="store_true", help="Validate session after injection")
|
|
1070
|
+
load_parser.add_argument("--runtime", action="store_true", help="Load into RuntimeEngine")
|
|
1071
|
+
load_parser.add_argument("--test-api", action="store_true", help="Test API after loading")
|
|
1072
|
+
load_parser.add_argument("--visible", action="store_true", help="Show browser window")
|
|
1073
|
+
load_parser.add_argument("--profile-dir", help="Browser profile directory")
|
|
1074
|
+
load_parser.add_argument("--no-local-storage", action="store_true", help="Skip localStorage injection if present")
|
|
1075
|
+
|
|
1076
|
+
# Inject Profile
|
|
1077
|
+
inject_parser = subparsers.add_parser("inject-profile", help="Inject cookies directly into browser profile")
|
|
1078
|
+
inject_parser.add_argument("--session", "-s", required=True, help="Path to session file")
|
|
1079
|
+
inject_parser.add_argument("--profile", "-p", required=True, help="Browser profile path")
|
|
1080
|
+
inject_parser.add_argument("--browser", "-b", choices=["chrome", "brave", "edge", "firefox", "opera", "vivaldi"],
|
|
1081
|
+
default="chrome", help="Browser name")
|
|
1082
|
+
inject_parser.add_argument("--no-backup", action="store_true", help="Skip backup creation")
|
|
1083
|
+
inject_parser.add_argument("--dry-run", action="store_true", help="Show what would be injected without making changes")
|
|
1084
|
+
|
|
1085
|
+
# Encrypt
|
|
1086
|
+
encrypt_parser = subparsers.add_parser("encrypt", help="Encrypt session file")
|
|
1087
|
+
encrypt_parser.add_argument("--input", "-i", required=True, help="Input file path")
|
|
1088
|
+
encrypt_parser.add_argument("--output", "-o", help="Output file path")
|
|
1089
|
+
encrypt_parser.add_argument("--password", "-p", help="Encryption password")
|
|
1090
|
+
encrypt_parser.add_argument("--key-file", "-k", help="Password file")
|
|
1091
|
+
|
|
1092
|
+
# Decrypt
|
|
1093
|
+
decrypt_parser = subparsers.add_parser("decrypt", help="Decrypt session file")
|
|
1094
|
+
decrypt_parser.add_argument("--input", "-i", required=True, help="Encrypted file path")
|
|
1095
|
+
decrypt_parser.add_argument("--output", "-o", help="Output file path")
|
|
1096
|
+
decrypt_parser.add_argument("--password", "-p", help="Decryption password")
|
|
1097
|
+
decrypt_parser.add_argument("--key-file", "-k", help="Password file")
|
|
1098
|
+
|
|
1099
|
+
# Rekey
|
|
1100
|
+
rekey_parser = subparsers.add_parser("rekey", help="Change encryption password")
|
|
1101
|
+
rekey_parser.add_argument("--input", "-i", required=True, help="Encrypted file path")
|
|
1102
|
+
rekey_parser.add_argument("--output", "-o", help="Output file path")
|
|
1103
|
+
rekey_parser.add_argument("--old-password", help="Old password")
|
|
1104
|
+
rekey_parser.add_argument("--new-password", help="New password")
|
|
1105
|
+
rekey_parser.add_argument("--old-key-file", help="Old password file")
|
|
1106
|
+
rekey_parser.add_argument("--new-key-file", help="New password file")
|
|
1107
|
+
|
|
1108
|
+
# Batch Export
|
|
1109
|
+
batch_export_parser = subparsers.add_parser("batch-export", help="Batch export multiple sites")
|
|
1110
|
+
batch_export_parser.add_argument("--site-config", "-s", required=True, help="Site config JSON file")
|
|
1111
|
+
batch_export_parser.add_argument("--browser", "-b", choices=["chrome", "firefox", "edge", "brave"],
|
|
1112
|
+
default="firefox", help="Browser name")
|
|
1113
|
+
batch_export_parser.add_argument("--browser-path", help="Custom browser profile path")
|
|
1114
|
+
batch_export_parser.add_argument("--profile", "-p", help="Profile name")
|
|
1115
|
+
batch_export_parser.add_argument("--output", "-o", help="Output directory")
|
|
1116
|
+
batch_export_parser.add_argument("--extract-local-storage", action="store_true", help="Extract localStorage")
|
|
1117
|
+
|
|
1118
|
+
# Batch Load
|
|
1119
|
+
batch_load_parser = subparsers.add_parser("batch-load", help="Batch load multiple sessions")
|
|
1120
|
+
batch_load_parser.add_argument("--sessions-dir", "-d", required=True, help="Sessions directory")
|
|
1121
|
+
batch_load_parser.add_argument("--target-browser", "-t", choices=["chrome", "firefox", "edge", "brave"],
|
|
1122
|
+
default="chrome", help="Target browser")
|
|
1123
|
+
batch_load_parser.add_argument("--site-config", "-s", help="Site config JSON file for validation")
|
|
1124
|
+
batch_load_parser.add_argument("--profile-dir", help="Target profile directory")
|
|
1125
|
+
batch_load_parser.add_argument("--validate", action="store_true", help="Validate sessions")
|
|
1126
|
+
batch_load_parser.add_argument("--visible", action="store_true", help="Show browser window")
|
|
1127
|
+
|
|
1128
|
+
# Health Check
|
|
1129
|
+
health_parser = subparsers.add_parser("health", help="Check session health")
|
|
1130
|
+
health_parser.add_argument("--session", "-s", help="Single session file to check")
|
|
1131
|
+
health_parser.add_argument("--sessions-dir", "-d", help="Directory of sessions to check")
|
|
1132
|
+
|
|
1133
|
+
# Health Report
|
|
1134
|
+
health_report_parser = subparsers.add_parser("health-report", help="Batch health report for CI/CD")
|
|
1135
|
+
health_report_parser.add_argument("--session", "-s", help="Single session file to check")
|
|
1136
|
+
health_report_parser.add_argument("--sessions-dir", "-d", help="Directory of sessions to check")
|
|
1137
|
+
health_report_parser.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
|
|
1138
|
+
health_report_parser.add_argument("--min-health", type=float, default=0.5, help="Minimum health score (default: 0.5)")
|
|
1139
|
+
health_report_parser.add_argument("--max-expired", type=int, default=0, help="Max allowed expired cookies (default: 0)")
|
|
1140
|
+
health_report_parser.add_argument("--webhook", help="Send report to webhook URL")
|
|
1141
|
+
|
|
1142
|
+
# Refresh
|
|
1143
|
+
refresh_parser = subparsers.add_parser("refresh", help="Refresh session from source browser")
|
|
1144
|
+
refresh_parser.add_argument("--session", "-s", required=True, help="Session file to refresh")
|
|
1145
|
+
refresh_parser.add_argument("--source-browser", "-b", choices=["chrome", "firefox", "edge", "brave"],
|
|
1146
|
+
required=True, help="Source browser name")
|
|
1147
|
+
refresh_parser.add_argument("--source-browser-path", help="Custom source browser profile path")
|
|
1148
|
+
refresh_parser.add_argument("--source-profile", help="Source profile name")
|
|
1149
|
+
refresh_parser.add_argument("--site-config", help="Site config JSON file for filtering")
|
|
1150
|
+
|
|
1151
|
+
# Proxy
|
|
1152
|
+
proxy_parser = subparsers.add_parser("proxy", help="Start fingerprint-matched proxy server")
|
|
1153
|
+
proxy_parser.add_argument("--session", "-s", help="Path to .tokenade session file (single mode)")
|
|
1154
|
+
proxy_parser.add_argument("--all", action="store_true", help="Serve all sessions (multi-site mode)")
|
|
1155
|
+
proxy_parser.add_argument("--sessions-dir", "-d", help="Directory of .tokenade files (for --all)")
|
|
1156
|
+
proxy_parser.add_argument("--decrypt-password", help="Decrypt .tokenade file with this password")
|
|
1157
|
+
proxy_parser.add_argument("--mode", choices=["gui", "forward"], default="gui",
|
|
1158
|
+
help="Proxy mode: gui (browser GUI) or forward (HTTP_PROXY)")
|
|
1159
|
+
proxy_parser.add_argument("--port", "-p", type=int, default=9222, help="Port to listen on (default: 9222)")
|
|
1160
|
+
proxy_parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
|
|
1161
|
+
proxy_parser.add_argument("--legacy", action="store_true", help="Use legacy service-worker proxy (default: CDP)")
|
|
1162
|
+
proxy_parser.add_argument("--visible", action="store_true", help="Show browser window (CDP mode only)")
|
|
1163
|
+
proxy_parser.add_argument("--fingerprint", action="store_true", help="Enable TLS fingerprint matching via curl-cffi (breaks cf_clearance)")
|
|
1164
|
+
proxy_parser.add_argument("--impersonate", help="Browser to impersonate (e.g., chrome120, chrome131, firefox128, safari17_0)")
|
|
1165
|
+
proxy_parser.add_argument("--no-open-browser", action="store_true", help="Don't open browser automatically")
|
|
1166
|
+
proxy_parser.add_argument("--no-gui", action="store_true", help="Disable GUI mode (legacy proxy only)")
|
|
1167
|
+
proxy_parser.add_argument("--timeout", type=int, default=30, help="Request timeout in seconds (default: 30)")
|
|
1168
|
+
proxy_parser.add_argument("--auto-refresh", action="store_true", help="Auto-refresh session from source browser when cookies expire")
|
|
1169
|
+
proxy_parser.add_argument("--source-browser", help="Source browser for auto-refresh (e.g., firefox, chrome)")
|
|
1170
|
+
proxy_parser.add_argument("--source-profile", help="Source profile for auto-refresh (e.g., default, Profile 1)")
|
|
1171
|
+
proxy_parser.add_argument("--auto-navigate", action="store_true", help="Auto-navigate to site URL when proxy starts")
|
|
1172
|
+
proxy_parser.add_argument("--target-url", help="Override default navigation URL")
|
|
1173
|
+
proxy_parser.add_argument("--rotate", action="store_true", help="Enable session rotation across multiple sessions")
|
|
1174
|
+
proxy_parser.add_argument("--rotate-strategy", choices=["health-weighted", "round-robin", "random", "least-recently-used"],
|
|
1175
|
+
default="health-weighted", help="Rotation strategy (default: health-weighted)")
|
|
1176
|
+
proxy_parser.add_argument("--rotate-interval", type=int, default=300, help="Rotation interval in seconds (default: 300)")
|
|
1177
|
+
|
|
1178
|
+
# Sessions (subcommand group)
|
|
1179
|
+
sessions_parser = subparsers.add_parser("sessions", help="Manage multiple sessions")
|
|
1180
|
+
sessions_sub = sessions_parser.add_subparsers(dest="sessions_command", help="Session management commands")
|
|
1181
|
+
|
|
1182
|
+
# sessions list
|
|
1183
|
+
sessions_list_parser = sessions_sub.add_parser("list", help="List all sessions")
|
|
1184
|
+
sessions_list_parser.add_argument("--dir", "-d", default=".", help="Directory to search")
|
|
1185
|
+
sessions_list_parser.add_argument("--pattern", "-p", default="*.tokenade", help="File pattern")
|
|
1186
|
+
sessions_list_parser.add_argument("--recursive", "-r", action="store_true", help="Search subdirectories")
|
|
1187
|
+
sessions_list_parser.add_argument("--site", "-s", help="Filter by site name")
|
|
1188
|
+
sessions_list_parser.add_argument("--browser", "-b", help="Filter by source browser")
|
|
1189
|
+
|
|
1190
|
+
# sessions merge
|
|
1191
|
+
sessions_merge_parser = sessions_sub.add_parser("merge", help="Merge multiple sessions")
|
|
1192
|
+
sessions_merge_parser.add_argument("files", nargs="+", help="Session files to merge")
|
|
1193
|
+
sessions_merge_parser.add_argument("--output", "-o", required=True, help="Output file path")
|
|
1194
|
+
sessions_merge_parser.add_argument("--site-name", help="Site name for merged session")
|
|
1195
|
+
|
|
1196
|
+
# sessions rotate
|
|
1197
|
+
sessions_rotate_parser = sessions_sub.add_parser("rotate", help="Select next session (rotation)")
|
|
1198
|
+
sessions_rotate_parser.add_argument("files", nargs="+", help="Session files to rotate through")
|
|
1199
|
+
sessions_rotate_parser.add_argument("--strategy", choices=["round-robin", "random"], default="round-robin",
|
|
1200
|
+
help="Rotation strategy")
|
|
1201
|
+
sessions_rotate_parser.add_argument("--state-file", help="State file for round-robin")
|
|
1202
|
+
|
|
1203
|
+
# sessions stats
|
|
1204
|
+
sessions_stats_parser = sessions_sub.add_parser("stats", help="Show aggregate session statistics")
|
|
1205
|
+
sessions_stats_parser.add_argument("files", nargs="+", help="Session files to analyze")
|
|
1206
|
+
|
|
1207
|
+
# Share
|
|
1208
|
+
share_parser = subparsers.add_parser("share", help="Create shareable session link or QR code")
|
|
1209
|
+
share_parser.add_argument("--session", "-s", required=True, help="Session file to share")
|
|
1210
|
+
share_parser.add_argument("--output", "-o", help="Output file path (HTML or QR image)")
|
|
1211
|
+
share_parser.add_argument("--format", choices=["url", "html", "qr"], default="url",
|
|
1212
|
+
help="Output format: url (default), html, qr")
|
|
1213
|
+
share_parser.add_argument("--expiry", type=int, default=24, help="Link expiry in hours (default: 24)")
|
|
1214
|
+
share_parser.add_argument("--max-uses", type=int, default=0, help="Max uses (0 = unlimited)")
|
|
1215
|
+
share_parser.add_argument("--password", "-p", help="Password protect the link")
|
|
1216
|
+
share_parser.add_argument("--email-to", help="Comma-separated email recipients")
|
|
1217
|
+
share_parser.add_argument("--smtp-host", help="SMTP server host")
|
|
1218
|
+
share_parser.add_argument("--smtp-port", type=int, default=587, help="SMTP server port")
|
|
1219
|
+
share_parser.add_argument("--smtp-user", help="SMTP username")
|
|
1220
|
+
share_parser.add_argument("--smtp-password", help="SMTP password")
|
|
1221
|
+
share_parser.add_argument("--webhook-url", help="Webhook URL to notify")
|
|
1222
|
+
|
|
1223
|
+
# Unshare
|
|
1224
|
+
unshare_parser = subparsers.add_parser("unshare", help="Revoke a shared session")
|
|
1225
|
+
unshare_parser.add_argument("session_id", help="Session ID to revoke")
|
|
1226
|
+
unshare_parser.add_argument("--list", action="store_true", help="List all active shares")
|
|
1227
|
+
|
|
1228
|
+
# Import shared session
|
|
1229
|
+
import_parser = subparsers.add_parser("import", help="Import a shared session from URL")
|
|
1230
|
+
import_parser.add_argument("url", help="tokenade://share/ URL")
|
|
1231
|
+
import_parser.add_argument("--password", "-p", help="Decryption password")
|
|
1232
|
+
import_parser.add_argument("--output", "-o", help="Output file path")
|
|
1233
|
+
|
|
1234
|
+
# Validate Rules
|
|
1235
|
+
validate_rules_parser = subparsers.add_parser("validate-rules", help="Validate session with custom rules")
|
|
1236
|
+
validate_rules_parser.add_argument("--session", "-s", required=True, help="Session file to validate")
|
|
1237
|
+
validate_rules_parser.add_argument("--rules", "-r", required=True, help="Validation rules JSON file")
|
|
1238
|
+
validate_rules_parser.add_argument("--url", "-u", help="Target site URL")
|
|
1239
|
+
validate_rules_parser.add_argument("--update-baselines", action="store_true", help="Update screenshot baselines")
|
|
1240
|
+
|
|
1241
|
+
# Diff
|
|
1242
|
+
diff_parser = subparsers.add_parser("diff", help="Compare two session files")
|
|
1243
|
+
diff_parser.add_argument("session_a", help="First .tokenade file")
|
|
1244
|
+
diff_parser.add_argument("session_b", help="Second .tokenade file")
|
|
1245
|
+
diff_parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed differences")
|
|
1246
|
+
|
|
1247
|
+
# Plugin
|
|
1248
|
+
plugin_parser = subparsers.add_parser("plugin", help="Manage plugins")
|
|
1249
|
+
plugin_sub = plugin_parser.add_subparsers(dest="plugin_command", help="Plugin commands")
|
|
1250
|
+
|
|
1251
|
+
plugin_list_parser = plugin_sub.add_parser("list", help="List installed plugins")
|
|
1252
|
+
plugin_list_parser.add_argument("--available", action="store_true", help="Show available plugins from registry")
|
|
1253
|
+
|
|
1254
|
+
plugin_install_parser = plugin_sub.add_parser("install", help="Install a plugin")
|
|
1255
|
+
plugin_install_parser.add_argument("name", help="Plugin name to install")
|
|
1256
|
+
|
|
1257
|
+
plugin_uninstall_parser = plugin_sub.add_parser("uninstall", help="Uninstall a plugin")
|
|
1258
|
+
plugin_uninstall_parser.add_argument("name", help="Plugin name to uninstall")
|
|
1259
|
+
|
|
1260
|
+
plugin_info_parser = plugin_sub.add_parser("info", help="Show plugin details")
|
|
1261
|
+
plugin_info_parser.add_argument("name", help="Plugin name")
|
|
1262
|
+
|
|
1263
|
+
plugin_enable_parser = plugin_sub.add_parser("enable", help="Enable a disabled plugin")
|
|
1264
|
+
plugin_enable_parser.add_argument("name", help="Plugin name to enable")
|
|
1265
|
+
|
|
1266
|
+
plugin_disable_parser = plugin_sub.add_parser("disable", help="Disable a plugin without uninstalling")
|
|
1267
|
+
plugin_disable_parser.add_argument("name", help="Plugin name to disable")
|
|
1268
|
+
|
|
1269
|
+
plugin_update_parser = plugin_sub.add_parser("update", help="Update plugins from registry")
|
|
1270
|
+
plugin_update_parser.add_argument("name", nargs="?", default=None, help="Plugin name to update (all if omitted)")
|
|
1271
|
+
|
|
1272
|
+
plugin_sync_parser = plugin_sub.add_parser("sync", help="Install all available plugins from registry")
|
|
1273
|
+
plugin_sync_parser.add_argument("--force", action="store_true", help="Reinstall even if already installed")
|
|
1274
|
+
|
|
1275
|
+
plugin_reload_parser = plugin_sub.add_parser("reload", help="Reload a plugin")
|
|
1276
|
+
plugin_reload_parser.add_argument("name", help="Plugin name to reload")
|
|
1277
|
+
|
|
1278
|
+
# plugin search
|
|
1279
|
+
plugin_search_parser = plugin_sub.add_parser("search", help="Search plugins in marketplace")
|
|
1280
|
+
plugin_search_parser.add_argument("query", nargs="?", default="", help="Search query")
|
|
1281
|
+
plugin_search_parser.add_argument("--type", dest="plugin_type", help="Filter by plugin type")
|
|
1282
|
+
plugin_search_parser.add_argument("--category", help="Filter by category")
|
|
1283
|
+
plugin_search_parser.add_argument("--tags", help="Comma-separated tags to filter")
|
|
1284
|
+
plugin_search_parser.add_argument(
|
|
1285
|
+
"--sort",
|
|
1286
|
+
choices=["rating", "downloads", "name", "recent", "trending"],
|
|
1287
|
+
default="name",
|
|
1288
|
+
help="Sort order (default: name — registry has no fake download/rating metrics)",
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
# plugin categories
|
|
1292
|
+
plugin_sub.add_parser("categories", help="List plugin categories")
|
|
1293
|
+
|
|
1294
|
+
# plugin popular
|
|
1295
|
+
plugin_popular_parser = plugin_sub.add_parser("popular", help="Show most popular plugins")
|
|
1296
|
+
plugin_popular_parser.add_argument("--limit", "-n", type=int, default=10, help="Number of plugins to show")
|
|
1297
|
+
|
|
1298
|
+
# plugin recent
|
|
1299
|
+
plugin_recent_parser = plugin_sub.add_parser("recent", help="Show newest plugins")
|
|
1300
|
+
plugin_recent_parser.add_argument("--limit", "-n", type=int, default=10, help="Number of plugins to show")
|
|
1301
|
+
|
|
1302
|
+
# plugin rate
|
|
1303
|
+
plugin_rate_parser = plugin_sub.add_parser("rate", help="Rate a plugin (1-5 stars)")
|
|
1304
|
+
plugin_rate_parser.add_argument("name", help="Plugin name")
|
|
1305
|
+
plugin_rate_parser.add_argument("rating", type=float, help="Rating (1.0-5.0)")
|
|
1306
|
+
plugin_rate_parser.add_argument("--review", help="Written review")
|
|
1307
|
+
|
|
1308
|
+
# plugin ratings
|
|
1309
|
+
plugin_ratings_parser = plugin_sub.add_parser("ratings", help="View global ratings from GitHub")
|
|
1310
|
+
plugin_ratings_parser.add_argument("name", nargs="?", help="Plugin name (all if omitted)")
|
|
1311
|
+
|
|
1312
|
+
# plugin verify
|
|
1313
|
+
plugin_verify_parser = plugin_sub.add_parser("verify", help="Verify plugin integrity via checksums")
|
|
1314
|
+
plugin_verify_parser.add_argument("name", nargs="?", help="Plugin name (all if omitted)")
|
|
1315
|
+
|
|
1316
|
+
# plugin outdated
|
|
1317
|
+
plugin_sub.add_parser("outdated", help="Show plugins with available updates")
|
|
1318
|
+
|
|
1319
|
+
# plugin browse
|
|
1320
|
+
plugin_browse_parser = plugin_sub.add_parser("browse", help="Generate static HTML marketplace page")
|
|
1321
|
+
plugin_browse_parser.add_argument("--output", "-o", help="Output HTML file path")
|
|
1322
|
+
|
|
1323
|
+
# plugin test
|
|
1324
|
+
plugin_test_parser = plugin_sub.add_parser("test", help="Run tests on installed plugins")
|
|
1325
|
+
plugin_test_parser.add_argument("name", nargs="?", help="Plugin name (all if omitted)")
|
|
1326
|
+
|
|
1327
|
+
# Sync
|
|
1328
|
+
sync_parser = subparsers.add_parser("sync", help="Sync sessions from browser cookies")
|
|
1329
|
+
sync_sub = sync_parser.add_subparsers(dest="sync_command", help="Sync commands")
|
|
1330
|
+
|
|
1331
|
+
sync_add_parser = sync_sub.add_parser("add", help="Add a sync target")
|
|
1332
|
+
sync_add_parser.add_argument("--name", "-n", required=True, help="Target name (e.g., gmail)")
|
|
1333
|
+
sync_add_parser.add_argument("--domains", "-d", required=True, help="Comma-separated domains")
|
|
1334
|
+
sync_add_parser.add_argument("--browser", "-b", default="firefox", help="Browser (default: firefox)")
|
|
1335
|
+
sync_add_parser.add_argument("--profile", "-p", help="Browser profile name")
|
|
1336
|
+
sync_add_parser.add_argument("--output-dir", "-o", help="Output directory (default: ~/.tokenade/synced)")
|
|
1337
|
+
|
|
1338
|
+
sync_remove_parser = sync_sub.add_parser("remove", help="Remove a sync target")
|
|
1339
|
+
sync_remove_parser.add_argument("--name", "-n", required=True, help="Target name")
|
|
1340
|
+
|
|
1341
|
+
sync_sub.add_parser("list", help="List sync targets")
|
|
1342
|
+
sync_sub.add_parser("once", help="Run one-time sync")
|
|
1343
|
+
|
|
1344
|
+
sync_start_parser = sync_sub.add_parser("start", help="Start sync daemon")
|
|
1345
|
+
sync_start_parser.add_argument("--interval", "-i", type=int, default=60, help="Check interval in seconds")
|
|
1346
|
+
|
|
1347
|
+
# Monitor
|
|
1348
|
+
monitor_parser = subparsers.add_parser("monitor", help="Monitor session health in real-time")
|
|
1349
|
+
monitor_sub = monitor_parser.add_subparsers(dest="monitor_command", help="Monitor commands")
|
|
1350
|
+
|
|
1351
|
+
monitor_status_parser = monitor_sub.add_parser("status", help="Show monitoring status")
|
|
1352
|
+
monitor_status_parser.add_argument("--sessions-dir", "-d", help="Directory of sessions to check")
|
|
1353
|
+
monitor_status_parser.add_argument("--session", "-s", help="Single session file to check")
|
|
1354
|
+
|
|
1355
|
+
monitor_start_parser = monitor_sub.add_parser("start", help="Start background monitoring")
|
|
1356
|
+
monitor_start_parser.add_argument("--sessions-dir", "-d", help="Directory of sessions to monitor")
|
|
1357
|
+
monitor_start_parser.add_argument("--session", "-s", help="Single session file to monitor")
|
|
1358
|
+
monitor_start_parser.add_argument("--interval", "-i", type=int, default=60, help="Check interval in seconds")
|
|
1359
|
+
monitor_start_parser.add_argument("--auto-refresh", action="store_true", help="Auto-refresh expiring sessions")
|
|
1360
|
+
|
|
1361
|
+
monitor_stop_parser = monitor_sub.add_parser("stop", help="Stop background monitoring")
|
|
1362
|
+
|
|
1363
|
+
monitor_history_parser = monitor_sub.add_parser("history", help="Show monitor event history")
|
|
1364
|
+
monitor_history_parser.add_argument("--sessions-dir", "-d", help="Sessions directory")
|
|
1365
|
+
monitor_history_parser.add_argument("--limit", "-l", type=int, default=50, help="Max events to show")
|
|
1366
|
+
|
|
1367
|
+
monitor_predict_parser = monitor_sub.add_parser("predict", help="Predict session expiry")
|
|
1368
|
+
monitor_predict_parser.add_argument("--sessions-dir", "-d", help="Sessions directory")
|
|
1369
|
+
monitor_predict_parser.add_argument("--session", "-s", help="Single session file")
|
|
1370
|
+
|
|
1371
|
+
# Analytics
|
|
1372
|
+
analytics_parser = subparsers.add_parser("analytics", help="Session usage analytics")
|
|
1373
|
+
analytics_sub = analytics_parser.add_subparsers(dest="analytics_command", help="Analytics commands")
|
|
1374
|
+
|
|
1375
|
+
analytics_report_parser = analytics_sub.add_parser("report", help="Show usage report")
|
|
1376
|
+
analytics_report_parser.add_argument("--days", "-d", type=int, default=30, help="Report period in days")
|
|
1377
|
+
analytics_report_parser.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
|
|
1378
|
+
|
|
1379
|
+
analytics_session_parser = analytics_sub.add_parser("session", help="Show analytics for a session")
|
|
1380
|
+
analytics_session_parser.add_argument("session_id", help="Session ID to analyze")
|
|
1381
|
+
|
|
1382
|
+
analytics_cleanup_parser = analytics_sub.add_parser("cleanup", help="Remove old analytics data")
|
|
1383
|
+
analytics_cleanup_parser.add_argument("--max-age", type=int, default=90, help="Max age in days")
|
|
1384
|
+
|
|
1385
|
+
# Shell Completion
|
|
1386
|
+
completion_parser = subparsers.add_parser("completion", help="Generate shell completion scripts")
|
|
1387
|
+
completion_parser.add_argument("shell", choices=["bash", "zsh", "fish"], help="Shell type")
|
|
1388
|
+
|
|
1389
|
+
# OAuth Refresh
|
|
1390
|
+
refresh_oauth_parser = subparsers.add_parser("refresh-oauth", help="Refresh OAuth tokens using stored refresh token")
|
|
1391
|
+
refresh_oauth_parser.add_argument("--session", "-s", required=True, help="Session file to refresh")
|
|
1392
|
+
|
|
1393
|
+
# OAuth Config
|
|
1394
|
+
oauth_config_parser = subparsers.add_parser("oauth-config", help="Configure OAuth settings for a session")
|
|
1395
|
+
oauth_config_parser.add_argument("--session", "-s", required=True, help="Session file to configure")
|
|
1396
|
+
oauth_config_parser.add_argument("--client-id", help="OAuth client ID")
|
|
1397
|
+
oauth_config_parser.add_argument("--client-secret", help="OAuth client secret")
|
|
1398
|
+
oauth_config_parser.add_argument("--token-endpoint", help="OAuth token endpoint URL")
|
|
1399
|
+
oauth_config_parser.add_argument("--scopes", help="Comma-separated OAuth scopes")
|
|
1400
|
+
oauth_config_parser.add_argument("--show", action="store_true", help="Show current OAuth config")
|
|
1401
|
+
|
|
1402
|
+
# Batch Refresh
|
|
1403
|
+
batch_refresh_parser = subparsers.add_parser("batch-refresh", help="Refresh multiple sessions with rate limiting")
|
|
1404
|
+
batch_refresh_parser.add_argument("--sessions-dir", "-d", default="sessions", help="Sessions directory")
|
|
1405
|
+
batch_refresh_parser.add_argument("--max-workers", "-w", type=int, default=3, help="Max parallel workers")
|
|
1406
|
+
batch_refresh_parser.add_argument("--delay", type=float, default=1.0, help="Delay between refreshes (seconds)")
|
|
1407
|
+
batch_refresh_parser.add_argument("--source-browser", "-b", default="firefox", help="Source browser for cookie refresh")
|
|
1408
|
+
batch_refresh_parser.add_argument("--source-profile", "-p", help="Source profile name")
|
|
1409
|
+
batch_refresh_parser.add_argument("--force", action="store_true", help="Force refresh even if not expired")
|
|
1410
|
+
batch_refresh_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt")
|
|
1411
|
+
|
|
1412
|
+
# CI/CD
|
|
1413
|
+
cicd_parser = subparsers.add_parser("cicd", help="Generate CI/CD workflow files")
|
|
1414
|
+
cicd_parser.add_argument("--generate-all", action="store_true", help="Generate all workflow files")
|
|
1415
|
+
cicd_parser.add_argument("--workflow-type", choices=["github", "gitlab", "cron"], default="github", help="Workflow type")
|
|
1416
|
+
cicd_parser.add_argument("--sessions-dir", default="sessions", help="Sessions directory")
|
|
1417
|
+
cicd_parser.add_argument("--interval-hours", type=int, default=6, help="Refresh interval in hours")
|
|
1418
|
+
cicd_parser.add_argument("--source-browser", default="firefox", help="Source browser for cookie refresh")
|
|
1419
|
+
cicd_parser.add_argument("--output", "-o", help="Output file path")
|
|
1420
|
+
cicd_parser.add_argument("--output-dir", default=".tokenade/ci", help="Output directory for --generate-all")
|
|
1421
|
+
|
|
1422
|
+
# CI Runner
|
|
1423
|
+
ci_parser = subparsers.add_parser("ci", help="Session CI runner")
|
|
1424
|
+
ci_sub = ci_parser.add_subparsers(dest="ci_action")
|
|
1425
|
+
|
|
1426
|
+
ci_run = ci_sub.add_parser("run", help="Run CI pipeline from tokenade.yml")
|
|
1427
|
+
ci_run.add_argument("--config", default="tokenade.yml", help="Config file path")
|
|
1428
|
+
ci_run.add_argument("--format", choices=["text", "json", "junit"], help="Override output format")
|
|
1429
|
+
|
|
1430
|
+
ci_init = ci_sub.add_parser("init", help="Create a starter tokenade.yml")
|
|
1431
|
+
ci_init.add_argument("--config", default="tokenade.yml", help="Config file path")
|
|
1432
|
+
|
|
1433
|
+
ci_validate = ci_sub.add_parser("validate", help="Validate tokenade.yml schema")
|
|
1434
|
+
ci_validate.add_argument("--config", default="tokenade.yml", help="Config file path")
|
|
1435
|
+
|
|
1436
|
+
ci_lint = ci_sub.add_parser("lint", help="Lint tokenade.yml for common issues")
|
|
1437
|
+
ci_lint.add_argument("--config", default="tokenade.yml", help="Config file path")
|
|
1438
|
+
|
|
1439
|
+
# Fleet Management
|
|
1440
|
+
fleet_parser = subparsers.add_parser("fleet", help="Fleet status across containers/pods")
|
|
1441
|
+
fleet_sub = fleet_parser.add_subparsers(dest="fleet_action")
|
|
1442
|
+
|
|
1443
|
+
fleet_status = fleet_sub.add_parser("status", help="Show all sessions across containers")
|
|
1444
|
+
fleet_status.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
|
|
1445
|
+
|
|
1446
|
+
fleet_health = fleet_sub.add_parser("health", help="Run health checks across the fleet")
|
|
1447
|
+
fleet_health.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
|
|
1448
|
+
|
|
1449
|
+
fleet_sub.add_parser("refresh", help="Trigger refresh in all running containers")
|
|
1450
|
+
|
|
1451
|
+
fleet_logs = fleet_sub.add_parser("logs", help="Get logs from a container")
|
|
1452
|
+
fleet_logs.add_argument("container", nargs="?", help="Container name")
|
|
1453
|
+
fleet_logs.add_argument("--lines", "-n", type=int, default=50, help="Number of log lines")
|
|
1454
|
+
|
|
1455
|
+
# Autopsy (Forensics)
|
|
1456
|
+
autopsy_parser = subparsers.add_parser("autopsy", help="Analyze why a session died")
|
|
1457
|
+
autopsy_parser.add_argument("--session", "-s", required=True, help="Session file to analyze")
|
|
1458
|
+
autopsy_parser.add_argument("--compare", "-c", help="Compare with another session file")
|
|
1459
|
+
autopsy_parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
|
|
1460
|
+
|
|
1461
|
+
# TUI
|
|
1462
|
+
tui_parser = subparsers.add_parser("tui", help="Launch interactive terminal UI")
|
|
1463
|
+
tui_parser.add_argument("tui_mode", nargs="?", default="full",
|
|
1464
|
+
choices=["full", "marketplace", "sessions"],
|
|
1465
|
+
help="TUI mode")
|
|
1466
|
+
|
|
1467
|
+
# Validate Session
|
|
1468
|
+
validate_session_parser = subparsers.add_parser("validate-session", help="Validate session files for CI/CD")
|
|
1469
|
+
validate_session_parser.add_argument("--session", "-s", help="Single session file to validate")
|
|
1470
|
+
validate_session_parser.add_argument("--sessions-dir", "-d", help="Directory of sessions to validate")
|
|
1471
|
+
validate_session_parser.add_argument("--min-health", type=float, default=0.5, help="Minimum health score (0.0-1.0)")
|
|
1472
|
+
validate_session_parser.add_argument("--max-expired", type=int, default=0, help="Max allowed expired cookies")
|
|
1473
|
+
validate_session_parser.add_argument("--require-oauth", action="store_true", help="Require OAuth config")
|
|
1474
|
+
validate_session_parser.add_argument("--max-age-hours", type=float, help="Max session age in hours")
|
|
1475
|
+
validate_session_parser.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
|
|
1476
|
+
|
|
1477
|
+
# Encrypted Refresh
|
|
1478
|
+
encrypted_refresh_parser = subparsers.add_parser("encrypted-refresh", help="Refresh encrypted session files")
|
|
1479
|
+
encrypted_refresh_parser.add_argument("--session", "-s", help="Single session file to refresh")
|
|
1480
|
+
encrypted_refresh_parser.add_argument("--sessions-dir", "-d", help="Directory of sessions to refresh")
|
|
1481
|
+
encrypted_refresh_parser.add_argument("--password", "-p", help="Decryption password")
|
|
1482
|
+
encrypted_refresh_parser.add_argument("--key-file", "-k", help="Key file path")
|
|
1483
|
+
encrypted_refresh_parser.add_argument("--source-browser", "-b", default="firefox", help="Source browser for cookie refresh")
|
|
1484
|
+
encrypted_refresh_parser.add_argument("--force", action="store_true", help="Force refresh even if not expired")
|
|
1485
|
+
|
|
1486
|
+
# CloakBrowser
|
|
1487
|
+
cloak_parser = subparsers.add_parser("cloak", help="CloakBrowser stealth browser management")
|
|
1488
|
+
cloak_sub = cloak_parser.add_subparsers(dest="cloak_action")
|
|
1489
|
+
|
|
1490
|
+
cloak_info = cloak_sub.add_parser("info", help="Show CloakBrowser status and binary info")
|
|
1491
|
+
cloak_info.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
|
|
1492
|
+
cloak_sub.add_parser("install", help="Download/update CloakBrowser binary")
|
|
1493
|
+
|
|
1494
|
+
cloak_serve = cloak_sub.add_parser("serve", help="Start CDP server (cloakserve)")
|
|
1495
|
+
cloak_serve.add_argument("--port", "-p", type=int, default=9222, help="Port to listen on")
|
|
1496
|
+
cloak_serve.add_argument("--proxy", help="Upstream proxy URL")
|
|
1497
|
+
cloak_serve.add_argument("--headless", action="store_true", default=True, help="Run headless")
|
|
1498
|
+
cloak_serve.add_argument("--visible", action="store_true", help="Run headed")
|
|
1499
|
+
cloak_serve.add_argument("--idle-timeout", type=int, help="Idle timeout in seconds")
|
|
1500
|
+
|
|
1501
|
+
# Launch Undetectable Browser
|
|
1502
|
+
launch_parser = subparsers.add_parser("launch", help="Launch undetectable system browser with CDP")
|
|
1503
|
+
launch_parser.add_argument("--browser", "-b", default="chrome", help="Browser to launch (chrome, firefox, brave, edge)")
|
|
1504
|
+
launch_parser.add_argument("--session", "-s", help="Session file to inject cookies from")
|
|
1505
|
+
launch_parser.add_argument("--url", "-u", help="URL to navigate to after injection")
|
|
1506
|
+
launch_parser.add_argument("--port", "-p", type=int, default=9222, help="CDP debugging port")
|
|
1507
|
+
launch_parser.add_argument("--profile-dir", help="Custom profile directory")
|
|
1508
|
+
launch_parser.add_argument("--visible", action="store_true", help="Show browser window (default unless --headless)")
|
|
1509
|
+
launch_parser.add_argument("--headless", action="store_true", help="Run headless (no window)")
|
|
1510
|
+
launch_parser.add_argument("--extra-args", help="Extra browser args (comma-separated)")
|
|
1511
|
+
launch_parser.add_argument("--browser-path", help="Path to browser executable")
|
|
1512
|
+
launch_parser.add_argument("--proxy", help="Upstream proxy URL (e.g. socks5://user:pass@host:port)")
|
|
1513
|
+
launch_parser.add_argument("--proxy-file", help="Proxy list file for rotation (one proxy per line)")
|
|
1514
|
+
launch_parser.add_argument("--proxy-rotate", action="store_true", help="Enable proxy rotation")
|
|
1515
|
+
launch_parser.add_argument(
|
|
1516
|
+
"--proxy-strategy", choices=["round-robin", "random", "health-weighted", "sticky"],
|
|
1517
|
+
default="health-weighted", help="Rotation strategy (default: health-weighted)"
|
|
1518
|
+
)
|
|
1519
|
+
launch_parser.add_argument("--humanize", action="store_true", help="Human-like mouse/keyboard/scroll (CloakBrowser)")
|
|
1520
|
+
launch_parser.add_argument("--geoip", action="store_true", help="Auto-detect timezone/locale from proxy IP (CloakBrowser)")
|
|
1521
|
+
launch_parser.add_argument("--no-cloak", action="store_true", help="Force Playwright + JS patches (skip CloakBrowser)")
|
|
1522
|
+
launch_parser.add_argument("--profile", help="Persistent profile directory (CloakBrowser)")
|
|
1523
|
+
launch_parser.add_argument("--decrypt-password", help="Decrypt .tokenade file with this password")
|
|
1524
|
+
launch_parser.add_argument(
|
|
1525
|
+
"--plugin",
|
|
1526
|
+
help="Force site handler plugin for launch (e.g. google-handler); auto-discovers when omitted",
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
# Refresh Browser (cookie-based session refresh)
|
|
1530
|
+
refresh_browser_parser = subparsers.add_parser("refresh-browser", help="Refresh session via undetectable browser (no OAuth needed)")
|
|
1531
|
+
refresh_browser_parser.add_argument("--session", "-s", required=True, help="Session file to refresh")
|
|
1532
|
+
refresh_browser_parser.add_argument("--browser", "-b", default="chrome", help="Browser to use (chrome, firefox, brave, edge)")
|
|
1533
|
+
refresh_browser_parser.add_argument("--url", "-u", help="Target URL (auto-detected from cookies if not specified)")
|
|
1534
|
+
refresh_browser_parser.add_argument("--port", "-p", type=int, default=9222, help="CDP debugging port")
|
|
1535
|
+
refresh_browser_parser.add_argument("--headless", action="store_true", help="Run headless (no window)")
|
|
1536
|
+
refresh_browser_parser.add_argument("--wait", "-w", type=int, default=8, help="Seconds to wait for session refresh (default: 8)")
|
|
1537
|
+
refresh_browser_parser.add_argument("--output", "-o", help="Output file (default: overwrite original)")
|
|
1538
|
+
refresh_browser_parser.add_argument("--plugin", help="Plugin to use for refresh (e.g., oauth2)")
|
|
1539
|
+
refresh_browser_parser.add_argument(
|
|
1540
|
+
"--plugin-arg", action="append", default=[], nargs=2, metavar=("KEY", "VALUE"),
|
|
1541
|
+
help="Plugin credential (repeatable): --plugin-arg client_id XXX"
|
|
1542
|
+
)
|
|
1543
|
+
refresh_browser_parser.add_argument("--proxy", help="Upstream proxy URL (e.g. socks5://user:pass@host:port)")
|
|
1544
|
+
refresh_browser_parser.add_argument("--proxy-file", help="Proxy list file for rotation (one proxy per line)")
|
|
1545
|
+
refresh_browser_parser.add_argument("--proxy-rotate", action="store_true", help="Enable proxy rotation")
|
|
1546
|
+
refresh_browser_parser.add_argument(
|
|
1547
|
+
"--proxy-strategy", choices=["round-robin", "random", "health-weighted", "sticky"],
|
|
1548
|
+
default="health-weighted", help="Rotation strategy (default: health-weighted)"
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
# Multi-Account Orchestration
|
|
1552
|
+
accounts_parser = subparsers.add_parser("accounts", help="Multi-account orchestration (list/status/refresh)")
|
|
1553
|
+
accounts_subparsers = accounts_parser.add_subparsers(dest="accounts_action")
|
|
1554
|
+
|
|
1555
|
+
# accounts list
|
|
1556
|
+
accounts_list = accounts_subparsers.add_parser("list", help="List all sessions")
|
|
1557
|
+
accounts_list.add_argument("--sessions-dir", "-d", default=".", help="Directory to scan (default: current)")
|
|
1558
|
+
accounts_list.add_argument("--site", "-s", help="Filter by site name")
|
|
1559
|
+
accounts_list.add_argument("--browser", "-b", help="Filter by browser")
|
|
1560
|
+
|
|
1561
|
+
# accounts status
|
|
1562
|
+
accounts_status = accounts_subparsers.add_parser("status", help="Show health/status of all sessions")
|
|
1563
|
+
accounts_status.add_argument("--sessions-dir", "-d", default=".", help="Directory to scan (default: current)")
|
|
1564
|
+
accounts_status.add_argument("--site", "-s", help="Filter by site name")
|
|
1565
|
+
|
|
1566
|
+
# accounts refresh
|
|
1567
|
+
accounts_refresh = accounts_subparsers.add_parser("refresh", help="Refresh all/specific sessions")
|
|
1568
|
+
accounts_refresh.add_argument("--sessions-dir", "-d", default=".", help="Directory to scan (default: current)")
|
|
1569
|
+
accounts_refresh.add_argument("--site", "-s", help="Filter by site name")
|
|
1570
|
+
accounts_refresh.add_argument("--browser", "-b", default="chrome", help="Browser to use for refresh")
|
|
1571
|
+
accounts_refresh.add_argument("--files", nargs="*", help="Specific session files to refresh")
|
|
1572
|
+
accounts_refresh.add_argument("--port", "-p", type=int, default=9222, help="Starting CDP port")
|
|
1573
|
+
accounts_refresh.add_argument("--visible", action="store_true", help="Show browser window")
|
|
1574
|
+
accounts_refresh.add_argument("--headless", action="store_true", default=True, help="Run headless (default)")
|
|
1575
|
+
accounts_refresh.add_argument("--wait", "-w", type=int, default=8, help="Seconds to wait per session")
|
|
1576
|
+
accounts_refresh.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
|
|
1577
|
+
accounts_refresh.add_argument("--plugin", help="Plugin to use for refresh (e.g., oauth2)")
|
|
1578
|
+
accounts_refresh.add_argument("--plugin-arg", action="append", default=[], nargs=2, metavar=("KEY", "VALUE"),
|
|
1579
|
+
help="Plugin credential (repeatable): --plugin-arg client_id XXX")
|
|
1580
|
+
accounts_refresh.add_argument("--proxy", help="Upstream proxy URL (e.g. socks5://user:pass@host:port)")
|
|
1581
|
+
accounts_refresh.add_argument("--proxy-file", help="Proxy list file for rotation (one proxy per line)")
|
|
1582
|
+
accounts_refresh.add_argument("--proxy-rotate", action="store_true", help="Enable proxy rotation")
|
|
1583
|
+
accounts_refresh.add_argument("--proxy-strategy", choices=["round-robin", "random", "health-weighted", "sticky"],
|
|
1584
|
+
default="health-weighted", help="Rotation strategy (default: health-weighted)")
|
|
1585
|
+
|
|
1586
|
+
# Patch Chrome Binary (remove cdc_ artifacts)
|
|
1587
|
+
patch_parser = subparsers.add_parser("patch-chrome", help="Patch Chrome/Chromium binary to remove cdc_ artifacts")
|
|
1588
|
+
patch_subparsers = patch_parser.add_subparsers(dest="patch_action")
|
|
1589
|
+
|
|
1590
|
+
# patch-chrome scan
|
|
1591
|
+
patch_scan = patch_subparsers.add_parser("scan", help="Scan binary for cdc_ artifacts")
|
|
1592
|
+
patch_scan.add_argument("--browser", "-b", default="chrome", help="Browser to scan (chrome, chromium, brave, edge)")
|
|
1593
|
+
patch_scan.add_argument("--binary", help="Path to browser binary (auto-detected if omitted)")
|
|
1594
|
+
|
|
1595
|
+
# patch-chrome patch
|
|
1596
|
+
patch_do = patch_subparsers.add_parser("patch", help="Patch binary to remove cdc_ artifacts")
|
|
1597
|
+
patch_do.add_argument("--browser", "-b", default="chrome", help="Browser to patch")
|
|
1598
|
+
patch_do.add_argument("--binary", help="Path to browser binary")
|
|
1599
|
+
patch_do.add_argument("--output", "-o", help="Output path for patched binary (default: <binary>.patched)")
|
|
1600
|
+
|
|
1601
|
+
# patch-chrome restore
|
|
1602
|
+
patch_restore = patch_subparsers.add_parser("restore", help="Restore binary from backup")
|
|
1603
|
+
patch_restore.add_argument("--browser", "-b", default="chrome", help="Browser to restore")
|
|
1604
|
+
patch_restore.add_argument("--binary", help="Path to browser binary")
|
|
1605
|
+
|
|
1606
|
+
# patch-chrome verify
|
|
1607
|
+
patch_verify = patch_subparsers.add_parser("verify", help="Verify binary patch status")
|
|
1608
|
+
patch_verify.add_argument("--browser", "-b", default="chrome", help="Browser to verify")
|
|
1609
|
+
patch_verify.add_argument("--binary", help="Path to browser binary")
|
|
1610
|
+
|
|
1611
|
+
# ── Mobile Import ────────────────────────────────────────────
|
|
1612
|
+
mobile_import_parser = subparsers.add_parser("mobile-import", help="Import sessions from mobile devices (Android/iOS)")
|
|
1613
|
+
mobile_import_parser.add_argument("--auto", action="store_true", help="Auto-detect device and browser")
|
|
1614
|
+
mobile_import_parser.add_argument("--list-devices", action="store_true", help="List connected mobile devices")
|
|
1615
|
+
mobile_import_parser.add_argument("--device", "-d", help="Device serial number")
|
|
1616
|
+
mobile_import_parser.add_argument("--browser", "-b", default="auto", help="Browser to extract from (chrome/firefox/samsung/brave/edge/safari, default: auto)")
|
|
1617
|
+
mobile_import_parser.add_argument("--domains", help="Comma-separated domains to filter")
|
|
1618
|
+
mobile_import_parser.add_argument("--output", "-o", help="Output .tokenade file path")
|
|
1619
|
+
mobile_import_parser.add_argument("--site-name", help="Site name override")
|
|
1620
|
+
mobile_import_parser.add_argument("--ios", action="store_true", help="Target iOS device (macOS only)")
|
|
1621
|
+
|
|
1622
|
+
# ── Daemon ──────────────────────────────────────────────────
|
|
1623
|
+
daemon_parser = subparsers.add_parser("daemon", help="Auto-refresh daemon (background session refresh)")
|
|
1624
|
+
daemon_subparsers = daemon_parser.add_subparsers(dest="daemon_action")
|
|
1625
|
+
|
|
1626
|
+
# daemon start
|
|
1627
|
+
daemon_start = daemon_subparsers.add_parser("start", help="Start daemon in background")
|
|
1628
|
+
daemon_start.add_argument("--interval", type=float, help="Check interval in minutes (default: 30)")
|
|
1629
|
+
daemon_start.add_argument("--webhook", help="Webhook URL for notifications")
|
|
1630
|
+
|
|
1631
|
+
# daemon stop
|
|
1632
|
+
daemon_subparsers.add_parser("stop", help="Stop the daemon")
|
|
1633
|
+
|
|
1634
|
+
# daemon status
|
|
1635
|
+
daemon_subparsers.add_parser("status", help="Show daemon status")
|
|
1636
|
+
|
|
1637
|
+
# daemon run-once
|
|
1638
|
+
daemon_subparsers.add_parser("run-once", help="Run single refresh cycle (foreground)")
|
|
1639
|
+
|
|
1640
|
+
# daemon add
|
|
1641
|
+
daemon_add = daemon_subparsers.add_parser("add", help="Add session to watch list")
|
|
1642
|
+
daemon_add.add_argument("session", help="Session file to watch")
|
|
1643
|
+
daemon_add.add_argument("--browser", "-b", default="chrome", help="Browser for refresh")
|
|
1644
|
+
daemon_add.add_argument(
|
|
1645
|
+
"--refresh-before", type=float, default=2.0,
|
|
1646
|
+
help="Refresh this many hours before expiry (default: 2.0)"
|
|
1647
|
+
)
|
|
1648
|
+
daemon_add.add_argument("--url", "-u", help="Target URL (auto-detected if not set)")
|
|
1649
|
+
daemon_add.add_argument("--site-name", help="Site name (auto-detected from filename)")
|
|
1650
|
+
|
|
1651
|
+
# daemon remove
|
|
1652
|
+
daemon_remove = daemon_subparsers.add_parser("remove", help="Remove session from watch list")
|
|
1653
|
+
daemon_remove.add_argument("session", help="Session file to remove")
|
|
1654
|
+
|
|
1655
|
+
# daemon list
|
|
1656
|
+
daemon_subparsers.add_parser("list", help="List all watched sessions")
|
|
1657
|
+
|
|
1658
|
+
# daemon logs
|
|
1659
|
+
daemon_logs = daemon_subparsers.add_parser("logs", help="View daemon logs")
|
|
1660
|
+
daemon_logs.add_argument("--lines", "-n", type=int, default=50, help="Number of lines to show")
|
|
1661
|
+
daemon_logs.add_argument("--follow", "-f", action="store_true", help="Follow log output (like tail -f)")
|
|
1662
|
+
|
|
1663
|
+
# ── Versions ────────────────────────────────────────────────
|
|
1664
|
+
versions_parser = subparsers.add_parser("versions", help="Session versioning (list/create/delete)")
|
|
1665
|
+
versions_subparsers = versions_parser.add_subparsers(dest="version_action")
|
|
1666
|
+
|
|
1667
|
+
versions_list = versions_subparsers.add_parser("list", help="List versions for a session")
|
|
1668
|
+
versions_list.add_argument("session", help="Session file")
|
|
1669
|
+
|
|
1670
|
+
versions_create = versions_subparsers.add_parser("create", help="Create a new version")
|
|
1671
|
+
versions_create.add_argument("session", help="Session file")
|
|
1672
|
+
versions_create.add_argument("--description", "-d", help="Version description")
|
|
1673
|
+
|
|
1674
|
+
versions_delete = versions_subparsers.add_parser("delete", help="Delete a version")
|
|
1675
|
+
versions_delete.add_argument("session", help="Session file")
|
|
1676
|
+
versions_delete.add_argument("version", type=int, help="Version number to delete")
|
|
1677
|
+
|
|
1678
|
+
# ── Rollback ────────────────────────────────────────────────
|
|
1679
|
+
rollback_parser = subparsers.add_parser("rollback", help="Rollback session to a specific version")
|
|
1680
|
+
rollback_parser.add_argument("session", help="Session file")
|
|
1681
|
+
rollback_parser.add_argument("version", type=int, help="Version number to restore")
|
|
1682
|
+
|
|
1683
|
+
# ── Session Diff (version comparison) ───────────────────────
|
|
1684
|
+
session_diff_parser = subparsers.add_parser("session-diff", help="Compare two session versions")
|
|
1685
|
+
session_diff_parser.add_argument("session", help="Session file")
|
|
1686
|
+
session_diff_parser.add_argument("version_a", type=int, help="First version number")
|
|
1687
|
+
session_diff_parser.add_argument("version_b", type=int, help="Second version number")
|
|
1688
|
+
|
|
1689
|
+
# ── Logs ────────────────────────────────────────────────────
|
|
1690
|
+
logs_parser = subparsers.add_parser("logs", help="View structured logs")
|
|
1691
|
+
logs_parser.add_argument("--lines", "-n", type=int, default=50, help="Number of recent lines to show (default: 50)")
|
|
1692
|
+
logs_parser.add_argument("--follow", "-f", action="store_true", help="Follow log output (like tail -f)")
|
|
1693
|
+
logs_parser.add_argument("--search", "-s", help="Search for text in logs")
|
|
1694
|
+
logs_parser.add_argument("--json", dest="json_output", action="store_true", help="Output in JSON format")
|
|
1695
|
+
logs_parser.add_argument("--log-file", help="Path to specific log file (default: ~/.tokenade/logs/tokenade.log)")
|
|
1696
|
+
logs_parser.add_argument("--list-files", action="store_true", help="List all log files")
|
|
1697
|
+
logs_parser.add_argument("--cleanup", type=int, metavar="DAYS", help="Remove log files older than N days")
|
|
1698
|
+
|
|
1699
|
+
# ── Clone Profile ──────────────────────────────────────────
|
|
1700
|
+
clone_parser = subparsers.add_parser("clone-profile", help="Clone browser profile with optional session injection")
|
|
1701
|
+
clone_parser.add_argument("source", nargs="?", help="Source profile directory (omit to use system default)")
|
|
1702
|
+
clone_parser.add_argument("--dest", "-d", required=True, help="Destination directory for the clone")
|
|
1703
|
+
clone_parser.add_argument("--browser", "-b", default="chrome", help="Browser name (chrome, firefox, brave, edge)")
|
|
1704
|
+
clone_parser.add_argument("--session", "-s", help="Session file to inject into the clone")
|
|
1705
|
+
clone_parser.add_argument("--profile", "-p", help="Profile name to clone (default: system default)")
|
|
1706
|
+
clone_parser.add_argument("--list-profiles", action="store_true", help="List available browser profiles")
|
|
1707
|
+
|
|
1708
|
+
# Container management
|
|
1709
|
+
container_parser = subparsers.add_parser("container", help="Docker container management")
|
|
1710
|
+
container_sub = container_parser.add_subparsers(dest="container_action")
|
|
1711
|
+
|
|
1712
|
+
container_start = container_sub.add_parser("start", help="Start proxy/API containers")
|
|
1713
|
+
container_start.add_argument("--sessions-dir", "-d", default=".", help="Directory with session files")
|
|
1714
|
+
container_start.add_argument("--proxy-port", type=int, default=9222, help="Starting proxy port")
|
|
1715
|
+
container_start.add_argument("--api-port", type=int, default=9224, help="API server port")
|
|
1716
|
+
container_start.add_argument("--no-api", action="store_true", help="Don't start API server")
|
|
1717
|
+
container_start.add_argument("--restart", default="unless-stopped", help="Restart policy (default: unless-stopped)")
|
|
1718
|
+
|
|
1719
|
+
container_stop = container_sub.add_parser("stop", help="Stop containers")
|
|
1720
|
+
container_stop.add_argument("--name", help="Container name to stop (default: all tokenade)")
|
|
1721
|
+
|
|
1722
|
+
container_sub.add_parser("restart", help="Restart containers")
|
|
1723
|
+
container_sub.add_parser("status", help="Show container status")
|
|
1724
|
+
|
|
1725
|
+
container_logs = container_sub.add_parser("logs", help="Tail container logs")
|
|
1726
|
+
container_logs.add_argument("name", help="Container name")
|
|
1727
|
+
container_logs.add_argument("--tail", "-n", type=int, default=100, help="Number of lines to tail")
|
|
1728
|
+
container_logs.add_argument("--follow", "-f", action="store_true", help="Follow log output")
|
|
1729
|
+
|
|
1730
|
+
container_refresh = container_sub.add_parser("refresh", help="Refresh sessions inside containers")
|
|
1731
|
+
container_refresh.add_argument("name", help="Container name")
|
|
1732
|
+
container_refresh.add_argument("--sessions-dir", default="/app/sessions", help="Sessions dir in container")
|
|
1733
|
+
|
|
1734
|
+
container_scale = container_sub.add_parser("scale", help="Scale proxy containers")
|
|
1735
|
+
container_scale.add_argument("replicas", type=int, help="Number of replicas")
|
|
1736
|
+
container_scale.add_argument("--sessions-dir", "-d", default=".", help="Directory with session files")
|
|
1737
|
+
|
|
1738
|
+
container_sub.add_parser("cleanup", help="Stop and remove all tokenade containers")
|
|
1739
|
+
|
|
1740
|
+
container_health = container_sub.add_parser("health", help="Check container health")
|
|
1741
|
+
container_health.add_argument("--watch", action="store_true", help="Continuous health monitoring")
|
|
1742
|
+
container_health.add_argument("--interval", type=int, default=60, help="Check interval in seconds")
|
|
1743
|
+
container_health.add_argument("--max-restarts", type=int, default=3, help="Max restarts before giving up")
|
|
1744
|
+
|
|
1745
|
+
container_gen = container_sub.add_parser("generate", help="Generate docker-compose override")
|
|
1746
|
+
container_gen.add_argument("--sessions-dir", "-d", default=".", help="Directory with session files")
|
|
1747
|
+
container_gen.add_argument("--output", "-o", help="Output file (default: stdout)")
|
|
1748
|
+
container_gen.add_argument("--base-port", type=int, default=9222, help="Starting port")
|
|
1749
|
+
|
|
1750
|
+
# Kubernetes management
|
|
1751
|
+
k8s_parser = subparsers.add_parser("k8s", help="Kubernetes deployment management")
|
|
1752
|
+
k8s_sub = k8s_parser.add_subparsers(dest="k8s_action")
|
|
1753
|
+
|
|
1754
|
+
k8s_deploy = k8s_sub.add_parser("deploy", help="Generate and apply K8s manifests")
|
|
1755
|
+
k8s_deploy.add_argument("--namespace", "-n", default="default", help="Kubernetes namespace")
|
|
1756
|
+
k8s_deploy.add_argument("--replicas", "-r", type=int, default=1, help="Number of replicas")
|
|
1757
|
+
k8s_deploy.add_argument("--image", default="tokenade:latest", help="Container image")
|
|
1758
|
+
k8s_deploy.add_argument("--port", type=int, default=9222, help="Proxy port")
|
|
1759
|
+
k8s_deploy.add_argument("--dry-run", action="store_true", help="Only generate YAML, don't apply")
|
|
1760
|
+
k8s_deploy.add_argument("--output", "-o", help="Output file for generated YAML")
|
|
1761
|
+
|
|
1762
|
+
k8s_status = k8s_sub.add_parser("status", help="Show deployment status")
|
|
1763
|
+
k8s_status.add_argument("--namespace", "-n", default="default", help="Kubernetes namespace")
|
|
1764
|
+
|
|
1765
|
+
k8s_scale = k8s_sub.add_parser("scale", help="Scale deployment")
|
|
1766
|
+
k8s_scale.add_argument("replicas", type=int, help="Number of replicas")
|
|
1767
|
+
k8s_scale.add_argument("--namespace", "-n", default="default", help="Kubernetes namespace")
|
|
1768
|
+
|
|
1769
|
+
k8s_logs = k8s_sub.add_parser("logs", help="Tail pod logs")
|
|
1770
|
+
k8s_logs.add_argument("--namespace", "-n", default="default", help="Kubernetes namespace")
|
|
1771
|
+
k8s_logs.add_argument("--tail", type=int, default=100, help="Number of lines to tail")
|
|
1772
|
+
|
|
1773
|
+
k8s_delete = k8s_sub.add_parser("delete", help="Delete deployment and service")
|
|
1774
|
+
k8s_delete.add_argument("--namespace", "-n", default="default", help="Kubernetes namespace")
|
|
1775
|
+
|
|
1776
|
+
k8s_sub.add_parser("pods", help="List pods")
|
|
1777
|
+
|
|
1778
|
+
profile_parser = subparsers.add_parser("profile", help="Manage browser profiles")
|
|
1779
|
+
profile_sub = profile_parser.add_subparsers(dest="profile_command", help="Profile commands")
|
|
1780
|
+
|
|
1781
|
+
profile_create = profile_sub.add_parser("create", help="Create a new profile")
|
|
1782
|
+
profile_create.add_argument("name", help="Profile name")
|
|
1783
|
+
profile_create.add_argument("--browser", "-b", default="chromium", help="Browser type (default: chromium)")
|
|
1784
|
+
profile_create.add_argument("--os", dest="os_name", default="windows", choices=["windows", "macos", "linux"], help="Target OS")
|
|
1785
|
+
profile_create.add_argument("--proxy", help="Proxy URL (e.g., socks5://user:pass@host:port)")
|
|
1786
|
+
profile_create.add_argument("--tags", help="Comma-separated tags")
|
|
1787
|
+
profile_create.add_argument("--notes", help="Profile notes")
|
|
1788
|
+
|
|
1789
|
+
profile_list = profile_sub.add_parser("list", help="List all profiles")
|
|
1790
|
+
profile_list.add_argument("--browser", "-b", help="Filter by browser type")
|
|
1791
|
+
profile_list.add_argument("--tag", "-t", help="Filter by tag")
|
|
1792
|
+
|
|
1793
|
+
profile_get = profile_sub.add_parser("get", help="Show profile details")
|
|
1794
|
+
profile_get.add_argument("name", help="Profile name")
|
|
1795
|
+
|
|
1796
|
+
profile_delete = profile_sub.add_parser("delete", help="Delete a profile")
|
|
1797
|
+
profile_delete.add_argument("name", help="Profile name")
|
|
1798
|
+
|
|
1799
|
+
profile_export = profile_sub.add_parser("export", help="Export profile to zip")
|
|
1800
|
+
profile_export.add_argument("name", help="Profile name")
|
|
1801
|
+
profile_export.add_argument("--output", "-o", help="Output file path")
|
|
1802
|
+
|
|
1803
|
+
profile_import = profile_sub.add_parser("import", help="Import profile from zip")
|
|
1804
|
+
profile_import.add_argument("archive", help="Archive file path")
|
|
1805
|
+
profile_import.add_argument("--name", "-n", help="Override profile name")
|
|
1806
|
+
|
|
1807
|
+
profile_recent = profile_sub.add_parser("recent", help="Show recently used profiles")
|
|
1808
|
+
profile_recent.add_argument("--limit", "-n", type=int, default=5, help="Number of profiles")
|
|
1809
|
+
|
|
1810
|
+
profile_sub.add_parser("stats", help="Show profile statistics")
|
|
1811
|
+
|
|
1812
|
+
stealth_parser = subparsers.add_parser("stealth", help="Browser stealth management")
|
|
1813
|
+
stealth_sub = stealth_parser.add_subparsers(dest="stealth_action")
|
|
1814
|
+
|
|
1815
|
+
stealth_test = stealth_sub.add_parser("test", help="Test stealth against detection sites")
|
|
1816
|
+
stealth_test.add_argument("--url", "-u", help="Custom test URL")
|
|
1817
|
+
stealth_test.add_argument("--browser", "-b", choices=["chrome", "firefox"], default="chrome")
|
|
1818
|
+
|
|
1819
|
+
stealth_report = stealth_sub.add_parser("report", help="Generate stealth report")
|
|
1820
|
+
stealth_report.add_argument("--output", "-o", help="Output file for report")
|
|
1821
|
+
stealth_report.add_argument("--browser", "-b", choices=["chrome", "firefox"], default="chrome")
|
|
1822
|
+
|
|
1823
|
+
stealth_battle = stealth_sub.add_parser("battle", help="Battle test against real detection sites")
|
|
1824
|
+
stealth_battle.add_argument("--browser", "-b", choices=["chromium", "firefox"], default="chromium")
|
|
1825
|
+
stealth_battle.add_argument("--site", "-s", action="append", help="Specific site(s) to test (default: all)")
|
|
1826
|
+
stealth_battle.add_argument("--timeout", "-t", type=int, default=30, help="Per-site timeout in seconds")
|
|
1827
|
+
stealth_battle.add_argument("--output", "-o", help="Output file for JSON report")
|
|
1828
|
+
|
|
1829
|
+
stealth_sub.add_parser("deps", help="Check stealth system dependencies")
|
|
1830
|
+
stealth_sub.add_parser("deps-install", help="Install missing stealth dependencies")
|
|
1831
|
+
|
|
1832
|
+
deps_parser = subparsers.add_parser("deps", help="System dependency management")
|
|
1833
|
+
deps_sub = deps_parser.add_subparsers(dest="deps_action")
|
|
1834
|
+
|
|
1835
|
+
deps_sub.add_parser("check", help="Check system dependencies")
|
|
1836
|
+
deps_install = deps_sub.add_parser("install", help="Install missing dependencies")
|
|
1837
|
+
deps_install.add_argument("--browser", "-b", choices=["chrome", "firefox"], default="chrome")
|
|
1838
|
+
deps_install.add_argument("--playwright", action="store_true", help="Install Playwright dependencies")
|
|
1839
|
+
|
|
1840
|
+
serve_parser = subparsers.add_parser("serve", help="Start API server")
|
|
1841
|
+
serve_parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
|
|
1842
|
+
serve_parser.add_argument("--port", "-p", type=int, default=9224, help="Port to listen on (default: 9224)")
|
|
1843
|
+
serve_parser.add_argument("--api-key", help="API key for authentication")
|
|
1844
|
+
serve_parser.add_argument("--sessions-dir", "-d", help="Sessions directory")
|
|
1845
|
+
serve_parser.add_argument("--cors", help="Allowed CORS origins (comma-separated)")
|
|
1846
|
+
|
|
1847
|
+
return parser
|
|
1848
|
+
|
|
1849
|
+
|
|
1850
|
+
def main():
|
|
1851
|
+
"""Main CLI entry point."""
|
|
1852
|
+
parser = _build_parser()
|
|
1853
|
+
args = parser.parse_args()
|
|
1854
|
+
|
|
1855
|
+
if not args.command:
|
|
1856
|
+
parser.print_help()
|
|
1857
|
+
sys.exit(1)
|
|
1858
|
+
|
|
1859
|
+
setup_logging._json_output = getattr(args, 'json_output', False)
|
|
1860
|
+
setup_logging(args.verbose)
|
|
1861
|
+
|
|
1862
|
+
commands = {
|
|
1863
|
+
"setup": cmd_setup,
|
|
1864
|
+
"config": cmd_config,
|
|
1865
|
+
"extract": cmd_extract,
|
|
1866
|
+
"transfer": cmd_transfer,
|
|
1867
|
+
"test": cmd_test,
|
|
1868
|
+
"fingerprint": cmd_fingerprint,
|
|
1869
|
+
"validate": cmd_validate,
|
|
1870
|
+
"export": cmd_export,
|
|
1871
|
+
"load": cmd_load,
|
|
1872
|
+
"inject-profile": cmd_inject_profile,
|
|
1873
|
+
"encrypt": cmd_encrypt,
|
|
1874
|
+
"decrypt": cmd_decrypt,
|
|
1875
|
+
"rekey": cmd_rekey,
|
|
1876
|
+
"batch-export": cmd_batch_export,
|
|
1877
|
+
"batch-load": cmd_batch_load,
|
|
1878
|
+
"health": cmd_health,
|
|
1879
|
+
"health-report": cmd_health_report,
|
|
1880
|
+
"refresh": cmd_refresh,
|
|
1881
|
+
"refresh-oauth": cmd_refresh_oauth,
|
|
1882
|
+
"oauth-config": cmd_oauth_config,
|
|
1883
|
+
"batch-refresh": cmd_batch_refresh,
|
|
1884
|
+
"encrypted-refresh": cmd_encrypted_refresh,
|
|
1885
|
+
"validate-session": cmd_validate_session,
|
|
1886
|
+
"cicd": cmd_cicd,
|
|
1887
|
+
"ci": cmd_ci,
|
|
1888
|
+
"launch": cmd_launch,
|
|
1889
|
+
"refresh-browser": cmd_refresh_browser,
|
|
1890
|
+
"accounts": cmd_accounts,
|
|
1891
|
+
"patch-chrome": cmd_patch_chrome,
|
|
1892
|
+
"mobile-import": cmd_mobile_import,
|
|
1893
|
+
"clone-profile": cmd_clone_profile,
|
|
1894
|
+
"daemon": cmd_daemon,
|
|
1895
|
+
"versions": cmd_versions,
|
|
1896
|
+
"rollback": cmd_rollback,
|
|
1897
|
+
"session-diff": cmd_session_diff,
|
|
1898
|
+
"logs": cmd_logs,
|
|
1899
|
+
"proxy": cmd_proxy,
|
|
1900
|
+
"sessions": cmd_sessions,
|
|
1901
|
+
"share": cmd_share,
|
|
1902
|
+
"unshare": cmd_unshare,
|
|
1903
|
+
"import": cmd_import,
|
|
1904
|
+
"sync": cmd_sync,
|
|
1905
|
+
"monitor": cmd_monitor,
|
|
1906
|
+
"analytics": cmd_analytics,
|
|
1907
|
+
"validate-rules": cmd_validate_rules,
|
|
1908
|
+
"diff": cmd_diff,
|
|
1909
|
+
"plugin": cmd_plugin,
|
|
1910
|
+
"completion": cmd_completion,
|
|
1911
|
+
"container": cmd_container,
|
|
1912
|
+
"k8s": cmd_k8s,
|
|
1913
|
+
"fleet": cmd_fleet,
|
|
1914
|
+
"cloak": cmd_cloak,
|
|
1915
|
+
"autopsy": cmd_autopsy,
|
|
1916
|
+
"tui": cmd_tui,
|
|
1917
|
+
"stealth": cmd_stealth,
|
|
1918
|
+
"deps": cmd_deps,
|
|
1919
|
+
"profile": cmd_profile,
|
|
1920
|
+
"serve": cmd_serve,
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
try:
|
|
1924
|
+
commands[args.command](args)
|
|
1925
|
+
except KeyboardInterrupt:
|
|
1926
|
+
print("\n\n⚠️ Interrupted by user")
|
|
1927
|
+
sys.exit(130)
|
|
1928
|
+
except Exception as e:
|
|
1929
|
+
from tokenade.core.errors import TokenadeError
|
|
1930
|
+
logger.exception("Command failed")
|
|
1931
|
+
if isinstance(e, TokenadeError):
|
|
1932
|
+
print(f"\n❌ {e}")
|
|
1933
|
+
else:
|
|
1934
|
+
print(f"\n❌ Unexpected error: {e}")
|
|
1935
|
+
print(" Run with --verbose for full traceback")
|
|
1936
|
+
print(" Logs: ~/.tokenade/logs/")
|
|
1937
|
+
sys.exit(1)
|