@heretek-ai/epistemic-swarm 0.7.25 → 1.0.2
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.
- package/index.ts +1 -0
- package/package.json +63 -139
- package/rpc.ts +1 -0
- package/server.ts +1 -0
- package/src/agents.ts +69 -0
- package/src/continue.ts +40 -0
- package/src/hooks.ts +283 -0
- package/src/panels.tsx +220 -0
- package/src/policy.ts +221 -0
- package/src/pr.ts +37 -0
- package/src/research.ts +63 -0
- package/src/rpc-def.ts +136 -0
- package/src/rpc.ts +331 -0
- package/src/runtime.ts +112 -0
- package/src/server.ts +311 -0
- package/src/session.ts +61 -0
- package/src/tools.ts +61 -0
- package/src/tui.tsx +206 -0
- package/tui.tsx +1 -0
- package/.agents/skills/brainstorming/SKILL.md +0 -19
- package/.agents/skills/code_audit/SKILL.md +0 -19
- package/.agents/skills/darkharvest/SKILL.md +0 -19
- package/.agents/skills/epistemic_search/SKILL.md +0 -19
- package/.agents/skills/factory/SKILL.md +0 -19
- package/.agents/skills/grilling/SKILL.md +0 -19
- package/.agents/skills/oss_scout/SKILL.md +0 -19
- package/.agents/skills/research_cache/SKILL.md +0 -19
- package/.agents/skills/swarm_config/SKILL.md +0 -19
- package/.claude-plugin/marketplace.json +0 -61
- package/.claude-plugin/plugin.json +0 -76
- package/.omp/README.md +0 -39
- package/.omp/SYSTEM.md +0 -17
- package/.omp/commands/audit.md +0 -10
- package/.omp/commands/brainstorming.md +0 -12
- package/.omp/commands/darkharvest.md +0 -11
- package/.omp/commands/domainexpansion.md +0 -9
- package/.omp/commands/factory.md +0 -9
- package/.omp/commands/grill.md +0 -9
- package/.omp/commands/scout.md +0 -10
- package/.omp/commands/swarm-config.md +0 -10
- package/.omp/commands/swarm.md +0 -9
- package/.omp/hooks/post/epistemic-audit.ts +0 -16
- package/.omp/hooks/pre/epistemic-redirect.ts +0 -21
- package/.omp/prompts/brainstorming.md +0 -8
- package/.omp/prompts/swarm.md +0 -8
- package/LICENSE +0 -126
- package/MARKETPLACE.md +0 -130
- package/README.md +0 -403
- package/bin/cli.js +0 -297
- package/config/claude-settings-patch.json +0 -10
- package/config/docker-compose.infra.yml +0 -23
- package/config/domain_packs/biopharma.json +0 -23
- package/config/domain_packs/legal.json +0 -19
- package/config/domain_packs/quant.json +0 -19
- package/config/mcp-research-servers.json +0 -33
- package/config/mcp_launcher.py +0 -153
- package/config/opencode-snippet.json +0 -156
- package/config/searxng_mcp.py +0 -92
- package/extensions/pi/index.js +0 -357
- package/hooks/hooks.json +0 -45
- package/install.sh +0 -473
- package/plugins/antigravity/README.md +0 -72
- package/plugins/antigravity/agents/alpha-thesis.md +0 -36
- package/plugins/antigravity/agents/beta-antithesis.md +0 -35
- package/plugins/antigravity/agents/brainstormer.md +0 -27
- package/plugins/antigravity/agents/code-auditor.md +0 -28
- package/plugins/antigravity/agents/darkharvester.md +0 -30
- package/plugins/antigravity/agents/epistemic-auditor.md +0 -28
- package/plugins/antigravity/agents/manager.md +0 -26
- package/plugins/antigravity/agents/oss-scout.md +0 -27
- package/plugins/antigravity/agents/programmer.md +0 -25
- package/plugins/antigravity/agents/qa-a.md +0 -22
- package/plugins/antigravity/agents/qa-b.md +0 -22
- package/plugins/antigravity/antigravity_hooks.py +0 -265
- package/plugins/antigravity/commands/audit.md +0 -11
- package/plugins/antigravity/commands/brainstorm.md +0 -9
- package/plugins/antigravity/commands/brainstorming.md +0 -10
- package/plugins/antigravity/commands/darkharvest.md +0 -12
- package/plugins/antigravity/commands/domainexpansion.md +0 -12
- package/plugins/antigravity/commands/factory.md +0 -14
- package/plugins/antigravity/commands/grill.md +0 -10
- package/plugins/antigravity/commands/scout.md +0 -11
- package/plugins/antigravity/commands/swarm-config.md +0 -9
- package/plugins/antigravity/commands/swarm.md +0 -12
- package/plugins/antigravity/hook_bus_bridge.js +0 -352
- package/plugins/antigravity/hooks.json +0 -44
- package/plugins/antigravity/mcp_config.json +0 -33
- package/plugins/antigravity/mcp_launcher.py +0 -58
- package/plugins/antigravity/plugin.json +0 -13
- package/plugins/antigravity/rules/AGENTS.md +0 -127
- package/plugins/antigravity/rules/epistemic-integrity.md +0 -16
- package/plugins/antigravity/skills/brainstorming/SKILL.md +0 -19
- package/plugins/antigravity/skills/code_audit/SKILL.md +0 -19
- package/plugins/antigravity/skills/darkharvest/SKILL.md +0 -19
- package/plugins/antigravity/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/antigravity/skills/factory/SKILL.md +0 -19
- package/plugins/antigravity/skills/grilling/SKILL.md +0 -19
- package/plugins/antigravity/skills/oss_scout/SKILL.md +0 -19
- package/plugins/antigravity/skills/research_cache/SKILL.md +0 -19
- package/plugins/antigravity/skills/swarm_config/SKILL.md +0 -19
- package/plugins/codex/AGENTS.md.snippet +0 -10
- package/plugins/codex/README.md +0 -37
- package/plugins/codex/config.toml.snippet +0 -28
- package/plugins/codex/openai.yaml +0 -24
- package/plugins/codex/skills/brainstorming/SKILL.md +0 -19
- package/plugins/codex/skills/code_audit/SKILL.md +0 -19
- package/plugins/codex/skills/darkharvest/SKILL.md +0 -19
- package/plugins/codex/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/codex/skills/factory/SKILL.md +0 -19
- package/plugins/codex/skills/grilling/SKILL.md +0 -19
- package/plugins/codex/skills/oss_scout/SKILL.md +0 -19
- package/plugins/codex/skills/research_cache/SKILL.md +0 -19
- package/plugins/codex/skills/swarm_config/SKILL.md +0 -19
- package/plugins/darkharvest/.claude-plugin/plugin.json +0 -15
- package/plugins/darkharvest/agents/harvest-proponent.md +0 -22
- package/plugins/darkharvest/agents/harvest-redteam.md +0 -20
- package/plugins/darkharvest/evals/teardown-verdict/graders/license-line.md +0 -6
- package/plugins/darkharvest/evals/teardown-verdict/graders/skill-fired.md +0 -5
- package/plugins/darkharvest/evals/teardown-verdict/prompt.md +0 -6
- package/plugins/darkharvest/skills/darkharvest/SKILL.md +0 -70
- package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +0 -289
- package/plugins/factory/.claude-plugin/plugin.json +0 -15
- package/plugins/factory/agents/factory-manager.md +0 -22
- package/plugins/factory/agents/programmer.md +0 -16
- package/plugins/factory/agents/qa-adversarial.md +0 -17
- package/plugins/factory/agents/qa-functional.md +0 -17
- package/plugins/factory/evals/gate-halt/graders/gates-first.md +0 -6
- package/plugins/factory/evals/gate-halt/graders/skill-fired.md +0 -5
- package/plugins/factory/evals/gate-halt/prompt.md +0 -6
- package/plugins/factory/skills/factory/SKILL.md +0 -61
- package/plugins/factory/skills/factory/scripts/factory.py +0 -300
- package/plugins/gemini/GEMINI.md +0 -16
- package/plugins/gemini/README.md +0 -20
- package/plugins/gemini/commands/audit.toml +0 -6
- package/plugins/gemini/commands/brainstorming.toml +0 -10
- package/plugins/gemini/commands/darkharvest.toml +0 -8
- package/plugins/gemini/commands/domainexpansion.toml +0 -7
- package/plugins/gemini/commands/factory.toml +0 -8
- package/plugins/gemini/commands/grill.toml +0 -6
- package/plugins/gemini/commands/scout.toml +0 -7
- package/plugins/gemini/commands/swarm-config.toml +0 -7
- package/plugins/gemini/commands/swarm.toml +0 -8
- package/plugins/gemini/gemini-extension.json +0 -38
- package/plugins/gemini/hooks/hooks.json +0 -11
- package/plugins/gemini/skills/brainstorming/SKILL.md +0 -19
- package/plugins/gemini/skills/code_audit/SKILL.md +0 -19
- package/plugins/gemini/skills/darkharvest/SKILL.md +0 -19
- package/plugins/gemini/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/gemini/skills/factory/SKILL.md +0 -19
- package/plugins/gemini/skills/grilling/SKILL.md +0 -19
- package/plugins/gemini/skills/oss_scout/SKILL.md +0 -19
- package/plugins/gemini/skills/research_cache/SKILL.md +0 -19
- package/plugins/gemini/skills/swarm_config/SKILL.md +0 -19
- package/plugins/opencode/analysis-bridge.js +0 -620
- package/plugins/opencode/config-io.js +0 -1651
- package/plugins/opencode/dep-health.js +0 -1039
- package/plugins/opencode/hook-bus.js +0 -496
- package/plugins/opencode/index.js +0 -2992
- package/plugins/opencode/tui.js +0 -1551
- package/plugins/opencode/weight-baseline.json +0 -16
- package/plugins/opencode/weight-signals.js +0 -769
- package/plugins/research-cache/.claude-plugin/plugin.json +0 -15
- package/plugins/research-cache/skills/research_cache/SKILL.md +0 -36
- package/plugins/research-cache/skills/research_cache/__init__.py +0 -0
- package/plugins/research-cache/skills/research_cache/hasher.py +0 -302
- package/plugins/socratic-grilling/.claude-plugin/plugin.json +0 -15
- package/plugins/socratic-grilling/skills/grilling/SKILL.md +0 -61
- package/plugins/socratic-grilling/skills/grilling/__init__.py +0 -0
- package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +0 -363
- package/prompts/agent_alpha_thesis.md +0 -70
- package/prompts/agent_beta_antithesis.md +0 -78
- package/prompts/agent_brainstormer.md +0 -143
- package/prompts/agent_code_auditor.md +0 -88
- package/prompts/agent_darkharvest.md +0 -140
- package/prompts/agent_oss_scout.md +0 -78
- package/prompts/antigravity_repo_factcheck.md +0 -143
- package/prompts/base_epistemic_system.md +0 -50
- package/prompts/epistemic_auditor.md +0 -74
- package/prompts/orchestrator.md +0 -70
- package/runner/__init__.py +0 -0
- package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
- package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
- package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
- package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/errors.cpython-311.pyc +0 -0
- package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
- package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
- package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
- package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
- package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/antigravity_hooks.py +0 -265
- package/runner/auctioneer.py +0 -169
- package/runner/auditor_engine.py +0 -711
- package/runner/claim_store.py +0 -417
- package/runner/claim_witness.py +0 -412
- package/runner/darkharvest_claims.py +0 -358
- package/runner/errors.py +0 -51
- package/runner/living_dossiers.py +0 -395
- package/runner/mcp_protocol.py +0 -188
- package/runner/mcp_server.py +0 -1447
- package/runner/path_safety.py +0 -62
- package/runner/pcrb.py +0 -290
- package/runner/pcrb_verify.py +0 -267
- package/runner/preflight.py +0 -1011
- package/runner/refinement.py +0 -580
- package/runner/research_swarm.py +0 -1848
- package/runner/retrieval_log.py +0 -90
- package/runner/schema_validate.py +0 -193
- package/runner/schemas.py +0 -630
- package/runner/state_machine.py +0 -463
- package/runner/tests/__pycache__/test_analysis_bridge.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dep_health.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hook_bus.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_nk_hardening.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
- package/runner/tests/fixtures/auction_objective.json +0 -35
- package/runner/tests/fixtures/borderline_claims.json +0 -27
- package/runner/tests/fixtures/config_validation_cases.json +0 -276
- package/runner/tests/fixtures/ddg_anomaly_page.html +0 -176
- package/runner/tests/fixtures/ddg_results_page.html +0 -28
- package/runner/tests/fixtures/divergence_objectives.json +0 -31
- package/runner/tests/test_analysis_bridge.py +0 -450
- package/runner/tests/test_antigravity_plugin.py +0 -361
- package/runner/tests/test_auction_order.py +0 -173
- package/runner/tests/test_backends.py +0 -457
- package/runner/tests/test_bet1_spike.py +0 -95
- package/runner/tests/test_claim_store.py +0 -162
- package/runner/tests/test_claim_witness.py +0 -186
- package/runner/tests/test_claude_plugin.py +0 -170
- package/runner/tests/test_config_schema.py +0 -879
- package/runner/tests/test_contract_tooling.py +0 -312
- package/runner/tests/test_darkharvest_audit.py +0 -395
- package/runner/tests/test_dep_health.py +0 -595
- package/runner/tests/test_domain_packs.py +0 -217
- package/runner/tests/test_dossier_contract.py +0 -196
- package/runner/tests/test_factory.py +0 -258
- package/runner/tests/test_fleet_seam.py +0 -113
- package/runner/tests/test_frontier_concurrency.py +0 -288
- package/runner/tests/test_frontier_mirror_standalone.py +0 -156
- package/runner/tests/test_grounding.py +0 -183
- package/runner/tests/test_hardening_contract.py +0 -391
- package/runner/tests/test_hook_bus.py +0 -956
- package/runner/tests/test_living_dossiers.py +0 -204
- package/runner/tests/test_manifest_concurrency.py +0 -187
- package/runner/tests/test_mcp_server.py +0 -257
- package/runner/tests/test_nk_hardening.py +0 -1046
- package/runner/tests/test_opencode_ux.py +0 -2438
- package/runner/tests/test_opencode_v2_registrars.py +0 -2157
- package/runner/tests/test_pcrb.py +0 -240
- package/runner/tests/test_phase08_waiver_reopens.py +0 -539
- package/runner/tests/test_refinement.py +0 -255
- package/runner/tests/test_search_anomaly.py +0 -342
- package/runner/tests/test_search_provider.py +0 -1534
- package/runner/tests/test_search_provider_plugin.py +0 -401
- package/runner/tests/test_swarm.py +0 -444
- package/runner/tests/test_swarm_config.py +0 -520
- package/runner/tests/test_sweep_regressions.py +0 -453
- package/runner/tests/test_webcache.py +0 -271
- package/runner/tests/test_weight_signals.py +0 -401
- package/schemas/alpha_dossier.schema.json +0 -204
- package/schemas/audit_report.schema.json +0 -79
- package/schemas/beta_dossier.schema.json +0 -214
- package/schemas/config.schema.json +0 -311
- package/schemas/frontier.schema.json +0 -61
- package/schemas/manifest.schema.json +0 -49
- package/schemas/scope_manifest.schema.json +0 -47
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
- package/scripts/auction_experiment.py +0 -188
- package/scripts/bet1_advisory_spike.py +0 -442
- package/scripts/build_adapters.py +0 -417
- package/scripts/check_docs.py +0 -66
- package/scripts/divergence_experiment.py +0 -214
- package/scripts/gen_schemas.py +0 -69
- package/skills/brainstorming/SKILL.md +0 -106
- package/skills/brainstorming/__init__.py +0 -1
- package/skills/brainstorming/scripts/brainstorm.py +0 -201
- package/skills/code_audit/SKILL.md +0 -45
- package/skills/darkharvest/SKILL.md +0 -70
- package/skills/darkharvest/scripts/harvest.py +0 -289
- package/skills/epistemic_search/SKILL.md +0 -67
- package/skills/epistemic_search/__init__.py +0 -1
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/fetch.py +0 -156
- package/skills/epistemic_search/scripts/search.py +0 -551
- package/skills/epistemic_search/scripts/webcache.py +0 -339
- package/skills/factory/SKILL.md +0 -61
- package/skills/factory/scripts/factory.py +0 -300
- package/skills/grilling/SKILL.md +0 -61
- package/skills/grilling/__init__.py +0 -0
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/grilling/socratic_tree.py +0 -363
- package/skills/oss_scout/SKILL.md +0 -49
- package/skills/research_cache/SKILL.md +0 -36
- package/skills/research_cache/__init__.py +0 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/research_cache/hasher.py +0 -302
- package/skills/swarm_config/SKILL.md +0 -178
- package/skills/swarm_config/__init__.py +0 -35
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
- package/skills/swarm_config/configure.py +0 -1002
|
@@ -1,551 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Zero-API-Key Web Search Engine for IUMBTEMS.
|
|
4
|
-
DuckDuckGo HTML/lite parser with domain filtering and Claude Code XML formatting.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import re
|
|
10
|
-
import sys
|
|
11
|
-
import urllib.parse
|
|
12
|
-
import urllib.request
|
|
13
|
-
from html.parser import HTMLParser
|
|
14
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
-
from xml.sax.saxutils import escape as xml_escape
|
|
16
|
-
|
|
17
|
-
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
18
|
-
|
|
19
|
-
# ---------------------------------------------------------------------------
|
|
20
|
-
# DuckDuckGo anti-bot ("anomaly") detection
|
|
21
|
-
# ---------------------------------------------------------------------------
|
|
22
|
-
#
|
|
23
|
-
# DuckDuckGo Lite answers suspected automation with an HTTP-2xx image-selection
|
|
24
|
-
# CAPTCHA instead of results. The page is *not* empty, but it carries no
|
|
25
|
-
# ``result-link`` rows, so the parser yielded ``[]`` and callers could not tell
|
|
26
|
-
# a block apart from a legitimate no-hits query (they were swallowed as
|
|
27
|
-
# ``results: 0`` in ``.research/retrieval.jsonl``).
|
|
28
|
-
#
|
|
29
|
-
# We deliberately do NOT key off the bare word "anomaly": a normal result
|
|
30
|
-
# snippet may legitimately contain it — that is the false positive this phase
|
|
31
|
-
# guards against. Detection is anchored to page chrome that only the
|
|
32
|
-
# interstitial emits, captured verbatim in the recorded fixture
|
|
33
|
-
# ``runner/tests/fixtures/ddg_anomaly_page.html`` (14,213 bytes, HTTP 202):
|
|
34
|
-
#
|
|
35
|
-
# * ``id="challenge-form"`` — CAPTCHA submit form
|
|
36
|
-
# * ``id="img-form"`` — challenge image form
|
|
37
|
-
# * ``assets/anomaly/images/challenge/`` — challenge image asset path
|
|
38
|
-
# * ``anomaly-modal`` — challenge modal CSS class
|
|
39
|
-
#
|
|
40
|
-
# Detection additionally requires the page to be free of real ``result-link``
|
|
41
|
-
# anchors, so even an (implausible) results page whose snippet embeds one of the
|
|
42
|
-
# markers is still treated as a results page.
|
|
43
|
-
DDG_ANOMALY_MARKERS: Tuple[str, ...] = (
|
|
44
|
-
'id="challenge-form"',
|
|
45
|
-
'id="img-form"',
|
|
46
|
-
"assets/anomaly/images/challenge/",
|
|
47
|
-
"anomaly-modal",
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
DDG_BLOCKED_HINT = (
|
|
51
|
-
"DuckDuckGo served its anti-bot challenge page (BLOCKED), not results. "
|
|
52
|
-
"Back off and retry later, or fall back to another engine: set "
|
|
53
|
-
"search_engine to firecrawl or searxng in .research/config.json, or "
|
|
54
|
-
"export a BYO key (EXA_API_KEY / FIRECRAWL_API_KEY / PARALLEL_API_KEY / "
|
|
55
|
-
"TAVILY_API_KEY / TINYFISH_API_KEY)."
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
DDG_ERROR_HINT = (
|
|
59
|
-
"DuckDuckGo request failed (network/HTTP error). Retry with backoff or "
|
|
60
|
-
"fall back to firecrawl or searxng."
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class SearchBlocked(RuntimeError):
|
|
65
|
-
"""DuckDuckGo served its anti-bot interstitial instead of results.
|
|
66
|
-
|
|
67
|
-
Exposed for callers that want the block to be an error rather than an empty
|
|
68
|
-
list — see :func:`require_search_duckduckgo`. ``hint`` carries the
|
|
69
|
-
actionable fallback/backoff advice.
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
def __init__(
|
|
73
|
-
self, message: str = DDG_BLOCKED_HINT, hint: str = DDG_BLOCKED_HINT
|
|
74
|
-
) -> None:
|
|
75
|
-
super().__init__(message)
|
|
76
|
-
self.hint = hint
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _is_ddg_anomaly_page(html: str) -> bool:
|
|
80
|
-
"""True iff *html* is DDG's anti-bot interstitial rather than results.
|
|
81
|
-
|
|
82
|
-
Anchored to structural page chrome (see the module comment above), never to
|
|
83
|
-
the bare word "anomaly". An empty body or a page already carrying
|
|
84
|
-
``result-link`` anchors is a results page and is never flagged.
|
|
85
|
-
"""
|
|
86
|
-
if not html:
|
|
87
|
-
return False
|
|
88
|
-
if "result-link" in html:
|
|
89
|
-
return False
|
|
90
|
-
return any(marker in html for marker in DDG_ANOMALY_MARKERS)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _log_retrieval(kind: str, **fields) -> None:
|
|
94
|
-
"""Best-effort retrieval telemetry (see runner/retrieval_log.py)."""
|
|
95
|
-
try:
|
|
96
|
-
from pathlib import Path as _Path
|
|
97
|
-
|
|
98
|
-
root = _Path(__file__).resolve().parents[3]
|
|
99
|
-
if str(root) not in sys.path:
|
|
100
|
-
sys.path.insert(0, str(root))
|
|
101
|
-
from runner.retrieval_log import default_base_dir, log_event
|
|
102
|
-
|
|
103
|
-
log_event(default_base_dir(), kind, **fields)
|
|
104
|
-
except Exception:
|
|
105
|
-
pass
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _log_ddg_query(
|
|
109
|
-
status: str, effective_query: str, results: int, log: bool = True
|
|
110
|
-
) -> None:
|
|
111
|
-
"""Record a query event whose ``status`` distinguishes block from empty.
|
|
112
|
-
|
|
113
|
-
The plain ``results`` count is kept so ``retrieval_log.summarize`` still
|
|
114
|
-
works, but a block additionally carries ``status: "blocked"`` and
|
|
115
|
-
``blocked: true`` so the anomaly can no longer masquerade as a normal
|
|
116
|
-
zero-hit query.
|
|
117
|
-
|
|
118
|
-
``log=False`` suppresses telemetry entirely. The preflight/reachability
|
|
119
|
-
probe uses it so its own request is never counted as an agent search and
|
|
120
|
-
never leaks an event into whichever workspace happens to be the process
|
|
121
|
-
cwd (see ``runner/preflight.py:_engine_probe``).
|
|
122
|
-
"""
|
|
123
|
-
if not log:
|
|
124
|
-
return
|
|
125
|
-
fields: Dict[str, Any] = {
|
|
126
|
-
"query": effective_query,
|
|
127
|
-
"results": results,
|
|
128
|
-
"status": status,
|
|
129
|
-
"provider": "duckduckgo",
|
|
130
|
-
}
|
|
131
|
-
if status == "blocked":
|
|
132
|
-
fields["blocked"] = True
|
|
133
|
-
_log_retrieval("query", **fields)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
class DDGLiteParser(HTMLParser):
|
|
137
|
-
"""Linear, non-backtracking HTML parser for DuckDuckGo Lite search results.
|
|
138
|
-
|
|
139
|
-
DuckDuckGo Lite emits one table row per result: an ``<a class="result-link">``
|
|
140
|
-
holding the title and a ``<td class="result-snippet">`` holding the summary.
|
|
141
|
-
Results are assembled **per row** so that:
|
|
142
|
-
|
|
143
|
-
* a missing link or snippet cannot desynchronise every later result (the old
|
|
144
|
-
``zip(links, snippets)`` pairing shifted all subsequent rows), and
|
|
145
|
-
* nested markup inside a snippet — including a nested
|
|
146
|
-
``<a class="result-link">`` — never hijacks capture state (the old parser
|
|
147
|
-
reset its buffer mid-snippet and then never recorded the snippet at all).
|
|
148
|
-
|
|
149
|
-
Captured text is stored decoded (``convert_charrefs`` default); escaping is
|
|
150
|
-
done at emit time by :func:`format_xml`.
|
|
151
|
-
"""
|
|
152
|
-
|
|
153
|
-
def __init__(self):
|
|
154
|
-
super().__init__()
|
|
155
|
-
self.results: List[Dict[str, str]] = []
|
|
156
|
-
self._row_href: Optional[str] = None
|
|
157
|
-
self._row_title: List[str] = []
|
|
158
|
-
self._row_snippet: List[str] = []
|
|
159
|
-
self._capture: Optional[str] = None # "title" | "snippet" | None
|
|
160
|
-
self._depth = 0 # nesting depth of the tag that opened the capture
|
|
161
|
-
|
|
162
|
-
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]):
|
|
163
|
-
attr_dict = {k: v or "" for k, v in attrs}
|
|
164
|
-
classes = attr_dict.get("class", "").split()
|
|
165
|
-
|
|
166
|
-
if tag == "tr":
|
|
167
|
-
self.flush()
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
# While capturing a snippet, nested markup (including a nested
|
|
171
|
-
# result-link) stays inside the snippet. Only track nested <td> so the
|
|
172
|
-
# capture still ends at the *outer* </td>.
|
|
173
|
-
if self._capture == "snippet":
|
|
174
|
-
if tag == "td":
|
|
175
|
-
self._depth += 1
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
# Likewise for a title: a nested <a> must not close the capture early.
|
|
179
|
-
if self._capture == "title":
|
|
180
|
-
if tag == "a":
|
|
181
|
-
self._depth += 1
|
|
182
|
-
return
|
|
183
|
-
|
|
184
|
-
if tag == "a" and "result-link" in classes:
|
|
185
|
-
# Row-less fallback: a new result-link implies the previous result
|
|
186
|
-
# is complete even if no <tr> ever bounded it.
|
|
187
|
-
if self._row_href is not None:
|
|
188
|
-
self.flush()
|
|
189
|
-
self._capture = "title"
|
|
190
|
-
self._depth = 1
|
|
191
|
-
self._row_href = attr_dict.get("href", "")
|
|
192
|
-
self._row_title = []
|
|
193
|
-
elif tag == "td" and "result-snippet" in classes:
|
|
194
|
-
self._capture = "snippet"
|
|
195
|
-
self._depth = 1
|
|
196
|
-
self._row_snippet = []
|
|
197
|
-
|
|
198
|
-
def handle_endtag(self, tag: str):
|
|
199
|
-
if (self._capture == "title" and tag == "a") or (
|
|
200
|
-
self._capture == "snippet" and tag == "td"
|
|
201
|
-
):
|
|
202
|
-
self._depth -= 1
|
|
203
|
-
if self._depth <= 0:
|
|
204
|
-
self._capture = None
|
|
205
|
-
elif tag == "tr":
|
|
206
|
-
self.flush()
|
|
207
|
-
|
|
208
|
-
def handle_data(self, data: str):
|
|
209
|
-
if self._capture == "title":
|
|
210
|
-
self._row_title.append(data)
|
|
211
|
-
elif self._capture == "snippet":
|
|
212
|
-
self._row_snippet.append(data)
|
|
213
|
-
|
|
214
|
-
def flush(self):
|
|
215
|
-
"""Emit the in-progress row, if any. Call after ``feed()`` completes."""
|
|
216
|
-
if self._row_href is not None:
|
|
217
|
-
self.results.append(
|
|
218
|
-
{
|
|
219
|
-
"href": self._row_href,
|
|
220
|
-
"title": "".join(self._row_title).strip(),
|
|
221
|
-
"snippet": "".join(self._row_snippet).strip(),
|
|
222
|
-
}
|
|
223
|
-
)
|
|
224
|
-
self._row_href = None
|
|
225
|
-
self._row_title = []
|
|
226
|
-
self._row_snippet = []
|
|
227
|
-
self._capture = None
|
|
228
|
-
self._depth = 0
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _build_search_query(
|
|
232
|
-
query: str,
|
|
233
|
-
allowed_domains: Optional[List[str]] = None,
|
|
234
|
-
blocked_domains: Optional[List[str]] = None,
|
|
235
|
-
) -> str:
|
|
236
|
-
effective_query = query.strip()
|
|
237
|
-
if allowed_domains:
|
|
238
|
-
effective_query += " " + " OR ".join(f"site:{d}" for d in allowed_domains)
|
|
239
|
-
if blocked_domains:
|
|
240
|
-
effective_query += " " + " ".join(f"-site:{d}" for d in blocked_domains)
|
|
241
|
-
return effective_query
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def _env_search_timeout(default: float = 15.0) -> float:
|
|
245
|
-
"""Search HTTP timeout: `IUMBTEMS_SEARCH_TIMEOUT_S` env > built-in default.
|
|
246
|
-
|
|
247
|
-
The runner exports the env var from config `search_timeout_s` to spawned
|
|
248
|
-
children; unset/invalid keeps the historical 15s default.
|
|
249
|
-
"""
|
|
250
|
-
raw = os.environ.get("IUMBTEMS_SEARCH_TIMEOUT_S", "").strip()
|
|
251
|
-
if not raw:
|
|
252
|
-
return default
|
|
253
|
-
try:
|
|
254
|
-
value = float(raw)
|
|
255
|
-
except ValueError:
|
|
256
|
-
return default
|
|
257
|
-
return value if value > 0 else default
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def _fetch_ddg_html(effective_query: str, timeout: float = 15.0) -> str:
|
|
261
|
-
url = "https://lite.duckduckgo.com/lite/"
|
|
262
|
-
data = urllib.parse.urlencode({"q": effective_query}).encode("utf-8")
|
|
263
|
-
headers = {
|
|
264
|
-
"User-Agent": USER_AGENT,
|
|
265
|
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
266
|
-
"Accept-Language": "en-US,en;q=0.5",
|
|
267
|
-
"Referer": "https://lite.duckduckgo.com/",
|
|
268
|
-
}
|
|
269
|
-
req = urllib.request.Request(url, data=data, headers=headers)
|
|
270
|
-
try:
|
|
271
|
-
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
272
|
-
return resp.read().decode("utf-8", errors="replace")
|
|
273
|
-
except Exception as e:
|
|
274
|
-
sys.stderr.write(
|
|
275
|
-
f"[epistemic-search] Network error fetching search results: {e}\n"
|
|
276
|
-
)
|
|
277
|
-
return ""
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def _parse_clean_url(href: str) -> str:
|
|
281
|
-
m = re.search(r"uddg=([^&]+)", href)
|
|
282
|
-
if m:
|
|
283
|
-
return urllib.parse.unquote(m.group(1))
|
|
284
|
-
return href
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def _is_domain_allowed(
|
|
288
|
-
hostname: str,
|
|
289
|
-
allowed_domains: Optional[List[str]] = None,
|
|
290
|
-
blocked_domains: Optional[List[str]] = None,
|
|
291
|
-
) -> bool:
|
|
292
|
-
if allowed_domains and not any(
|
|
293
|
-
hostname == d or hostname.endswith("." + d) for d in allowed_domains
|
|
294
|
-
):
|
|
295
|
-
return False
|
|
296
|
-
if blocked_domains and any(
|
|
297
|
-
hostname == d or hostname.endswith("." + d) for d in blocked_domains
|
|
298
|
-
):
|
|
299
|
-
return False
|
|
300
|
-
return True
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def _parse_ddg_results(
|
|
304
|
-
html: str,
|
|
305
|
-
allowed_domains: Optional[List[str]] = None,
|
|
306
|
-
blocked_domains: Optional[List[str]] = None,
|
|
307
|
-
max_results: int = 10,
|
|
308
|
-
) -> List[Dict[str, str]]:
|
|
309
|
-
parser = DDGLiteParser()
|
|
310
|
-
parser.feed(html)
|
|
311
|
-
parser.flush()
|
|
312
|
-
|
|
313
|
-
results = []
|
|
314
|
-
for row in parser.results:
|
|
315
|
-
clean_url = _parse_clean_url(row["href"])
|
|
316
|
-
if not clean_url.startswith("http"):
|
|
317
|
-
continue
|
|
318
|
-
|
|
319
|
-
parsed_url = urllib.parse.urlparse(clean_url)
|
|
320
|
-
hostname = parsed_url.hostname or ""
|
|
321
|
-
if not _is_domain_allowed(hostname, allowed_domains, blocked_domains):
|
|
322
|
-
continue
|
|
323
|
-
|
|
324
|
-
results.append(
|
|
325
|
-
{
|
|
326
|
-
"title": row["title"],
|
|
327
|
-
"url": clean_url,
|
|
328
|
-
"snippet": row["snippet"],
|
|
329
|
-
}
|
|
330
|
-
)
|
|
331
|
-
if len(results) >= max_results:
|
|
332
|
-
break
|
|
333
|
-
|
|
334
|
-
return results
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
def search_duckduckgo_detailed(
|
|
338
|
-
query: str,
|
|
339
|
-
allowed_domains: Optional[List[str]] = None,
|
|
340
|
-
blocked_domains: Optional[List[str]] = None,
|
|
341
|
-
max_results: int = 10,
|
|
342
|
-
log: bool = True,
|
|
343
|
-
timeout: Optional[float] = None,
|
|
344
|
-
) -> Dict[str, Any]:
|
|
345
|
-
"""Search DuckDuckGo Lite and report *why* a run produced no results.
|
|
346
|
-
|
|
347
|
-
Returns a typed dict::
|
|
348
|
-
|
|
349
|
-
{"status": "ok" | "empty" | "blocked" | "error",
|
|
350
|
-
"results": [...], "hint": "...", "query": "<effective query>"}
|
|
351
|
-
|
|
352
|
-
``"blocked"`` is DDG's anti-bot interstitial (an actionable hint, not an
|
|
353
|
-
empty success). ``"empty"`` is a genuine no-hits query. ``"error"`` is a
|
|
354
|
-
network/HTTP failure. Only this function preserves the distinction;
|
|
355
|
-
:func:`search_duckduckgo` keeps the legacy list contract for callers that
|
|
356
|
-
do not need it. ``timeout`` bounds the HTTP fetch; when omitted (None) the
|
|
357
|
-
`IUMBTEMS_SEARCH_TIMEOUT_S` env override (config `search_timeout_s`) is
|
|
358
|
-
honored, else the historical 15s default. An explicit value always wins —
|
|
359
|
-
the preflight probe wires its own bound through here.
|
|
360
|
-
"""
|
|
361
|
-
if timeout is None:
|
|
362
|
-
timeout = _env_search_timeout()
|
|
363
|
-
effective_query = _build_search_query(query, allowed_domains, blocked_domains)
|
|
364
|
-
try:
|
|
365
|
-
html = _fetch_ddg_html(effective_query, timeout=timeout)
|
|
366
|
-
except TypeError:
|
|
367
|
-
# An injected/older ``_fetch_ddg_html`` that takes only the query.
|
|
368
|
-
html = _fetch_ddg_html(effective_query)
|
|
369
|
-
|
|
370
|
-
if not html:
|
|
371
|
-
results: List[Dict[str, str]] = []
|
|
372
|
-
status = "error"
|
|
373
|
-
hint = DDG_ERROR_HINT
|
|
374
|
-
elif _is_ddg_anomaly_page(html):
|
|
375
|
-
results = []
|
|
376
|
-
status = "blocked"
|
|
377
|
-
hint = DDG_BLOCKED_HINT
|
|
378
|
-
else:
|
|
379
|
-
results = _parse_ddg_results(
|
|
380
|
-
html, allowed_domains, blocked_domains, max_results
|
|
381
|
-
)
|
|
382
|
-
status = "ok" if results else "empty"
|
|
383
|
-
hint = "" if results else "No results parsed for this query."
|
|
384
|
-
|
|
385
|
-
_log_ddg_query(status, effective_query, len(results), log=log)
|
|
386
|
-
return {
|
|
387
|
-
"status": status,
|
|
388
|
-
"results": results,
|
|
389
|
-
"hint": hint,
|
|
390
|
-
"query": effective_query,
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def search_duckduckgo(
|
|
395
|
-
query: str,
|
|
396
|
-
allowed_domains: Optional[List[str]] = None,
|
|
397
|
-
blocked_domains: Optional[List[str]] = None,
|
|
398
|
-
max_results: int = 10,
|
|
399
|
-
log: bool = True,
|
|
400
|
-
timeout: Optional[float] = None,
|
|
401
|
-
) -> List[Dict[str, str]]:
|
|
402
|
-
"""Execute search query against DuckDuckGo Lite without API keys.
|
|
403
|
-
|
|
404
|
-
Preserves the original list contract for callers that only want results.
|
|
405
|
-
When DDG serves its anti-bot interstitial this returns ``[]`` *and* warns on
|
|
406
|
-
stderr; callers that must act on the block should use
|
|
407
|
-
:func:`search_duckduckgo_detailed` or :func:`require_search_duckduckgo`.
|
|
408
|
-
"""
|
|
409
|
-
outcome = search_duckduckgo_detailed(
|
|
410
|
-
query, allowed_domains, blocked_domains, max_results, log=log, timeout=timeout
|
|
411
|
-
)
|
|
412
|
-
if outcome["status"] == "blocked":
|
|
413
|
-
sys.stderr.write(f"[epistemic-search] BLOCKED: {outcome['hint']}\n")
|
|
414
|
-
return outcome["results"]
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def require_search_duckduckgo(
|
|
418
|
-
query: str,
|
|
419
|
-
allowed_domains: Optional[List[str]] = None,
|
|
420
|
-
blocked_domains: Optional[List[str]] = None,
|
|
421
|
-
max_results: int = 10,
|
|
422
|
-
log: bool = True,
|
|
423
|
-
timeout: Optional[float] = None,
|
|
424
|
-
) -> List[Dict[str, str]]:
|
|
425
|
-
"""Like :func:`search_duckduckgo` but raises :class:`SearchBlocked`.
|
|
426
|
-
|
|
427
|
-
Use when an anti-bot page is an error condition (fall back to another
|
|
428
|
-
engine, then retry) rather than an empty success.
|
|
429
|
-
"""
|
|
430
|
-
outcome = search_duckduckgo_detailed(
|
|
431
|
-
query, allowed_domains, blocked_domains, max_results, log=log, timeout=timeout
|
|
432
|
-
)
|
|
433
|
-
if outcome["status"] == "blocked":
|
|
434
|
-
raise SearchBlocked(outcome["hint"], hint=outcome["hint"])
|
|
435
|
-
return outcome["results"]
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
def format_xml(results: List[Dict[str, str]]) -> str:
|
|
439
|
-
"""Format results as Claude Code <search_results> XML.
|
|
440
|
-
|
|
441
|
-
Every field is XML-escaped at emit time. The parser stores decoded text, so
|
|
442
|
-
without this a snippet like ``A & B`` becomes ``A & B`` and produces
|
|
443
|
-
malformed markup (bare ``&``); URLs containing query strings have the same
|
|
444
|
-
problem.
|
|
445
|
-
"""
|
|
446
|
-
lines = ["<search_results>"]
|
|
447
|
-
for r in results:
|
|
448
|
-
lines.append(" <result>")
|
|
449
|
-
lines.append(f" <title>{xml_escape(r['title'])}</title>")
|
|
450
|
-
lines.append(f" <url>{xml_escape(r['url'])}</url>")
|
|
451
|
-
lines.append(f" <snippet>{xml_escape(r['snippet'])}</snippet>")
|
|
452
|
-
lines.append(" </result>")
|
|
453
|
-
lines.append("</search_results>")
|
|
454
|
-
return "\n".join(lines)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
def _read_stdin_payload() -> Tuple[str, Optional[List[str]], Optional[List[str]]]:
|
|
458
|
-
if sys.stdin.isatty():
|
|
459
|
-
return "", None, None
|
|
460
|
-
try:
|
|
461
|
-
data = sys.stdin.read().strip()
|
|
462
|
-
if not data:
|
|
463
|
-
return "", None, None
|
|
464
|
-
try:
|
|
465
|
-
payload = json.loads(data)
|
|
466
|
-
return (
|
|
467
|
-
payload.get("query", ""),
|
|
468
|
-
payload.get("allowed_domains"),
|
|
469
|
-
payload.get("blocked_domains"),
|
|
470
|
-
)
|
|
471
|
-
except json.JSONDecodeError:
|
|
472
|
-
return data, None, None
|
|
473
|
-
except Exception:
|
|
474
|
-
return "", None, None
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
def _parse_cli_args(
|
|
478
|
-
args: List[str],
|
|
479
|
-
default_query: str = "",
|
|
480
|
-
default_allowed: Optional[List[str]] = None,
|
|
481
|
-
default_blocked: Optional[List[str]] = None,
|
|
482
|
-
) -> Tuple[str, Optional[List[str]], Optional[List[str]], bool, bool]:
|
|
483
|
-
query = default_query
|
|
484
|
-
allowed = default_allowed
|
|
485
|
-
blocked = default_blocked
|
|
486
|
-
output_json = False
|
|
487
|
-
output_status = False
|
|
488
|
-
idx = 0
|
|
489
|
-
while idx < len(args):
|
|
490
|
-
arg = args[idx]
|
|
491
|
-
if arg == "--json":
|
|
492
|
-
output_json = True
|
|
493
|
-
elif arg == "--status":
|
|
494
|
-
output_status = True
|
|
495
|
-
elif arg == "--allowed-domain" and idx + 1 < len(args):
|
|
496
|
-
allowed = (allowed or []) + [args[idx + 1]]
|
|
497
|
-
idx += 1
|
|
498
|
-
elif arg == "--blocked-domain" and idx + 1 < len(args):
|
|
499
|
-
blocked = (blocked or []) + [args[idx + 1]]
|
|
500
|
-
idx += 1
|
|
501
|
-
elif not query and not arg.startswith("--"):
|
|
502
|
-
query = arg
|
|
503
|
-
idx += 1
|
|
504
|
-
return query, allowed, blocked, output_json, output_status
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
def main():
|
|
508
|
-
std_query, std_allowed, std_blocked = _read_stdin_payload()
|
|
509
|
-
query, allowed, blocked, output_json, output_status = _parse_cli_args(
|
|
510
|
-
sys.argv[1:], std_query, std_allowed, std_blocked
|
|
511
|
-
)
|
|
512
|
-
|
|
513
|
-
if not query:
|
|
514
|
-
sys.stderr.write(
|
|
515
|
-
'Usage: search.py [options] <query>\nOr pipe JSON: echo \'{"query":"..."}\' | search.py\n'
|
|
516
|
-
"Options: --json (results as JSON), --status (typed status + hint),\n"
|
|
517
|
-
" --allowed-domain D, --blocked-domain D\n"
|
|
518
|
-
)
|
|
519
|
-
sys.exit(1)
|
|
520
|
-
|
|
521
|
-
outcome = search_duckduckgo_detailed(
|
|
522
|
-
query=query,
|
|
523
|
-
allowed_domains=allowed,
|
|
524
|
-
blocked_domains=blocked,
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
if outcome["status"] in ("blocked", "error"):
|
|
528
|
-
sys.stderr.write(
|
|
529
|
-
f"[epistemic-search] {outcome['status'].upper()}: {outcome['hint']}\n"
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
if output_status:
|
|
533
|
-
print(
|
|
534
|
-
json.dumps(
|
|
535
|
-
{
|
|
536
|
-
"status": outcome["status"],
|
|
537
|
-
"query": outcome["query"],
|
|
538
|
-
"results": len(outcome["results"]),
|
|
539
|
-
"hint": outcome["hint"],
|
|
540
|
-
},
|
|
541
|
-
indent=2,
|
|
542
|
-
)
|
|
543
|
-
)
|
|
544
|
-
elif output_json:
|
|
545
|
-
print(json.dumps(outcome["results"], indent=2))
|
|
546
|
-
else:
|
|
547
|
-
print(format_xml(outcome["results"]))
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if __name__ == "__main__":
|
|
551
|
-
main()
|