@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,289 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Darkharvest fetch helper: clone-to-temp + allowlisted scan + SHA-256 cache.
|
|
4
|
-
|
|
5
|
-
Read-only. Never installs dependencies. Never writes outside .research/
|
|
6
|
-
(except the temp clone, which is dropped). Enforces per-repo caps so
|
|
7
|
-
`--max-repos 10 --depth 3` style defaults cannot OOM the host.
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
python3 skills/darkharvest/scripts/harvest.py --repo <url> [--show-context]
|
|
11
|
-
python3 skills/darkharvest/scripts/harvest.py --repo <url> --export <path>
|
|
12
|
-
|
|
13
|
-
Closed / non-cloneable targets: prints a metadata-only stub with a
|
|
14
|
-
[NEGATIVE_KNOWLEDGE] gap note instead of failing.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import argparse
|
|
18
|
-
import json
|
|
19
|
-
import os
|
|
20
|
-
import re
|
|
21
|
-
import shutil
|
|
22
|
-
import subprocess
|
|
23
|
-
import sys
|
|
24
|
-
import tempfile
|
|
25
|
-
from datetime import datetime, timezone
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
|
|
28
|
-
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
|
29
|
-
if str(PROJECT_ROOT) not in sys.path:
|
|
30
|
-
sys.path.insert(0, str(PROJECT_ROOT))
|
|
31
|
-
|
|
32
|
-
from skills.research_cache.hasher import SourceHasher # noqa: E402
|
|
33
|
-
|
|
34
|
-
SKIP_DIRS = {
|
|
35
|
-
".git",
|
|
36
|
-
"node_modules",
|
|
37
|
-
"__pycache__",
|
|
38
|
-
".venv",
|
|
39
|
-
"venv",
|
|
40
|
-
"dist",
|
|
41
|
-
"build",
|
|
42
|
-
"target",
|
|
43
|
-
".next",
|
|
44
|
-
".research",
|
|
45
|
-
"coverage",
|
|
46
|
-
}
|
|
47
|
-
MANIFEST_FILES = [
|
|
48
|
-
"package.json",
|
|
49
|
-
"pyproject.toml",
|
|
50
|
-
"Cargo.toml",
|
|
51
|
-
"go.mod",
|
|
52
|
-
"requirements.txt",
|
|
53
|
-
"LICENSE",
|
|
54
|
-
"LICENSE.md",
|
|
55
|
-
"README.md",
|
|
56
|
-
]
|
|
57
|
-
TEXT_EXTS = {".md", ".json", ".toml", ".yaml", ".yml", ".txt", ".py", ".js", ".ts"}
|
|
58
|
-
|
|
59
|
-
DEFAULT_MAX_FILES = 120
|
|
60
|
-
DEFAULT_MAX_BYTES = 400_000
|
|
61
|
-
DEFAULT_TIMEOUT = 120
|
|
62
|
-
DEFAULT_BASE_DIR = ".research"
|
|
63
|
-
MAX_SCAN_DEPTH = 3
|
|
64
|
-
MAX_SINGLE_FILE_BYTES = 100_000
|
|
65
|
-
SNIPPET_CHARS = 4000
|
|
66
|
-
|
|
67
|
-
SPDX_APACHE2 = "Apache-2.0"
|
|
68
|
-
PERMISSIVE_SPDX = frozenset({"MIT", SPDX_APACHE2, "BSD-3-Clause", "ISC"})
|
|
69
|
-
COPYLEFT_SPDX = frozenset({"GPL", "AGPL"})
|
|
70
|
-
|
|
71
|
-
SPDX_MAP = {
|
|
72
|
-
"mit": "MIT",
|
|
73
|
-
"apache-2.0": SPDX_APACHE2,
|
|
74
|
-
"apache 2.0": SPDX_APACHE2,
|
|
75
|
-
"bsd-3-clause": "BSD-3-Clause",
|
|
76
|
-
"bsd 3-clause": "BSD-3-Clause",
|
|
77
|
-
"isc": "ISC",
|
|
78
|
-
"gpl": "GPL",
|
|
79
|
-
"agpl": "AGPL",
|
|
80
|
-
"mpl": "MPL",
|
|
81
|
-
"lgpl": "LGPL",
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def sh(cmd, cwd=None, timeout=DEFAULT_TIMEOUT):
|
|
86
|
-
try:
|
|
87
|
-
r = subprocess.run(
|
|
88
|
-
cmd,
|
|
89
|
-
capture_output=True,
|
|
90
|
-
text=True,
|
|
91
|
-
cwd=str(cwd or PROJECT_ROOT),
|
|
92
|
-
timeout=timeout,
|
|
93
|
-
)
|
|
94
|
-
return r.returncode, (r.stdout or "").strip()
|
|
95
|
-
except Exception as exc: # noqa: BLE001
|
|
96
|
-
return 1, f"error: {exc}"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def detect_license(text):
|
|
100
|
-
low = (text or "").lower()
|
|
101
|
-
for key, spdx in SPDX_MAP.items():
|
|
102
|
-
if key in low:
|
|
103
|
-
return spdx
|
|
104
|
-
return "UNKNOWN"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def scan_tree(root, max_files=DEFAULT_MAX_FILES, max_bytes=DEFAULT_MAX_BYTES):
|
|
108
|
-
root = Path(root)
|
|
109
|
-
state = _ScanState()
|
|
110
|
-
for dirpath, dirnames, filenames in os.walk(root):
|
|
111
|
-
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
112
|
-
rel = Path(dirpath).relative_to(root)
|
|
113
|
-
if (len(rel.parts) if str(rel) != "." else 0) > MAX_SCAN_DEPTH:
|
|
114
|
-
dirnames[:] = []
|
|
115
|
-
continue
|
|
116
|
-
prefix = "" if str(rel) == "." else str(rel) + os.sep
|
|
117
|
-
for f in sorted(filenames):
|
|
118
|
-
if state.capped(max_files, max_bytes):
|
|
119
|
-
return state.finish(truncated=True)
|
|
120
|
-
_scan_file(dirpath, prefix + f, state)
|
|
121
|
-
return state.finish(truncated=False)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
class _ScanState:
|
|
125
|
-
"""Mutable accumulator for one scan_tree pass (keeps helpers small)."""
|
|
126
|
-
|
|
127
|
-
def __init__(self):
|
|
128
|
-
self.entries = []
|
|
129
|
-
self.total_bytes = 0
|
|
130
|
-
self.manifests = {}
|
|
131
|
-
self.license_text = ""
|
|
132
|
-
self.readme_head = ""
|
|
133
|
-
|
|
134
|
-
def capped(self, max_files, max_bytes):
|
|
135
|
-
return len(self.entries) >= max_files or self.total_bytes >= max_bytes
|
|
136
|
-
|
|
137
|
-
def finish(self, truncated):
|
|
138
|
-
return (
|
|
139
|
-
self.entries,
|
|
140
|
-
self.manifests,
|
|
141
|
-
self.license_text,
|
|
142
|
-
self.readme_head,
|
|
143
|
-
truncated,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _scan_file(dirpath, rel_entry, state):
|
|
148
|
-
filename = os.path.basename(rel_entry)
|
|
149
|
-
try:
|
|
150
|
-
size = (Path(dirpath) / filename).stat().st_size
|
|
151
|
-
except OSError:
|
|
152
|
-
return
|
|
153
|
-
if size > MAX_SINGLE_FILE_BYTES:
|
|
154
|
-
return
|
|
155
|
-
state.entries.append(rel_entry)
|
|
156
|
-
if filename in MANIFEST_FILES or Path(filename).suffix in TEXT_EXTS:
|
|
157
|
-
_capture_text(dirpath, filename, state)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _capture_text(dirpath, filename, state):
|
|
161
|
-
try:
|
|
162
|
-
content = (Path(dirpath) / filename).read_text(
|
|
163
|
-
encoding="utf-8", errors="replace"
|
|
164
|
-
)
|
|
165
|
-
except OSError:
|
|
166
|
-
return
|
|
167
|
-
state.total_bytes += len(content.encode("utf-8", errors="replace"))
|
|
168
|
-
snippet = content[:SNIPPET_CHARS]
|
|
169
|
-
if filename in MANIFEST_FILES and filename not in state.manifests:
|
|
170
|
-
state.manifests[filename] = snippet
|
|
171
|
-
lowered = filename.lower()
|
|
172
|
-
if lowered.startswith("license") and not state.license_text:
|
|
173
|
-
state.license_text = snippet
|
|
174
|
-
if lowered == "readme.md" and not state.readme_head:
|
|
175
|
-
state.readme_head = snippet
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def harvest_repo(
|
|
179
|
-
url,
|
|
180
|
-
max_files=DEFAULT_MAX_FILES,
|
|
181
|
-
max_bytes=DEFAULT_MAX_BYTES,
|
|
182
|
-
timeout=DEFAULT_TIMEOUT,
|
|
183
|
-
base_dir=DEFAULT_BASE_DIR,
|
|
184
|
-
):
|
|
185
|
-
started = datetime.now(timezone.utc).isoformat()
|
|
186
|
-
tmp = tempfile.mkdtemp(prefix="darkharvest-")
|
|
187
|
-
try:
|
|
188
|
-
code, out = sh(["git", "clone", "--depth", "1", url, tmp], timeout=timeout)
|
|
189
|
-
if code != 0:
|
|
190
|
-
return {
|
|
191
|
-
"url": url,
|
|
192
|
-
"status": "metadata-only",
|
|
193
|
-
"generated_at": started,
|
|
194
|
-
"negative_knowledge": (
|
|
195
|
-
f"[NEGATIVE_KNOWLEDGE: clone failed for {url}; "
|
|
196
|
-
f"used metadata-only row. Detail: {out[:200]}]"
|
|
197
|
-
),
|
|
198
|
-
}
|
|
199
|
-
entries, manifests, license_text, readme_head, truncated = scan_tree(
|
|
200
|
-
Path(tmp), max_files=max_files, max_bytes=max_bytes
|
|
201
|
-
)
|
|
202
|
-
spdx = detect_license(license_text + manifests.get("package.json", ""))
|
|
203
|
-
harvestable = (
|
|
204
|
-
"depend-or-vendor" if spdx in PERMISSIVE_SPDX else "clean-room-rebuild-only"
|
|
205
|
-
)
|
|
206
|
-
hasher = SourceHasher(Path(base_dir))
|
|
207
|
-
cache_blob = (
|
|
208
|
-
f"# {url}\n\nSPDX: {spdx}\n\n"
|
|
209
|
-
f"## README head\n{readme_head[:2000]}\n\n"
|
|
210
|
-
f"## Tree sample ({len(entries)} entries)\n" + "\n".join(entries[:60])
|
|
211
|
-
)
|
|
212
|
-
source_hash = hasher.store_source(url, cache_blob, f"Darkharvest {url}")
|
|
213
|
-
warnings = []
|
|
214
|
-
if spdx in COPYLEFT_SPDX:
|
|
215
|
-
warnings.append(
|
|
216
|
-
"⚠️ LICENSE: strong copyleft — spec rebuild only, never vendor"
|
|
217
|
-
)
|
|
218
|
-
elif spdx == "UNKNOWN":
|
|
219
|
-
warnings.append("⚠️ LICENSE: unknown — treat as clean-room-rebuild-only")
|
|
220
|
-
if truncated:
|
|
221
|
-
warnings.append(
|
|
222
|
-
"⚠️ SCAN: truncated by file/byte caps; treat as partial evidence"
|
|
223
|
-
)
|
|
224
|
-
return {
|
|
225
|
-
"url": url,
|
|
226
|
-
"status": "scanned",
|
|
227
|
-
"generated_at": started,
|
|
228
|
-
"spdx": spdx,
|
|
229
|
-
"harvest_policy": harvestable,
|
|
230
|
-
"tree_entries": len(entries),
|
|
231
|
-
"truncated": truncated,
|
|
232
|
-
"source_hash": source_hash,
|
|
233
|
-
"readme_head": readme_head[:1500],
|
|
234
|
-
"manifests_seen": sorted(manifests.keys()),
|
|
235
|
-
"warnings": warnings,
|
|
236
|
-
}
|
|
237
|
-
finally:
|
|
238
|
-
shutil.rmtree(tmp, ignore_errors=True)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def main():
|
|
242
|
-
ap = argparse.ArgumentParser(description="Darkharvest competitor fetch helper")
|
|
243
|
-
ap.add_argument("--repo", required=True, help="Competitor repo URL to scan")
|
|
244
|
-
ap.add_argument("--show-context", action="store_true")
|
|
245
|
-
ap.add_argument("--export", default="")
|
|
246
|
-
ap.add_argument("--max-files", type=int, default=DEFAULT_MAX_FILES)
|
|
247
|
-
ap.add_argument("--max-bytes", type=int, default=DEFAULT_MAX_BYTES)
|
|
248
|
-
ap.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
|
|
249
|
-
ap.add_argument("--dir", default=DEFAULT_BASE_DIR)
|
|
250
|
-
args = ap.parse_args()
|
|
251
|
-
|
|
252
|
-
if not re.match(r"^https?://", args.repo):
|
|
253
|
-
print(f"WARN: {args.repo} is not an http(s) URL; metadata-only stub emitted.")
|
|
254
|
-
result = {
|
|
255
|
-
"url": args.repo,
|
|
256
|
-
"status": "metadata-only",
|
|
257
|
-
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
258
|
-
"negative_knowledge": (
|
|
259
|
-
f"[NEGATIVE_KNOWLEDGE: non-cloneable target {args.repo}]"
|
|
260
|
-
),
|
|
261
|
-
}
|
|
262
|
-
else:
|
|
263
|
-
result = harvest_repo(
|
|
264
|
-
args.repo,
|
|
265
|
-
max_files=args.max_files,
|
|
266
|
-
max_bytes=args.max_bytes,
|
|
267
|
-
timeout=args.timeout,
|
|
268
|
-
base_dir=args.dir,
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
if args.show_context or not args.export:
|
|
272
|
-
print(f"\n🌑 [Darkharvest] {result['url']} → {result['status']}")
|
|
273
|
-
for key in ("spdx", "harvest_policy", "tree_entries", "source_hash"):
|
|
274
|
-
if key in result:
|
|
275
|
-
print(f" {key}: {result[key]}")
|
|
276
|
-
for w in result.get("warnings", []):
|
|
277
|
-
print(f" {w}")
|
|
278
|
-
if result.get("negative_knowledge"):
|
|
279
|
-
print(f" {result['negative_knowledge'][:200]}")
|
|
280
|
-
|
|
281
|
-
if args.export:
|
|
282
|
-
out = Path(os.path.realpath(args.export))
|
|
283
|
-
out.parent.mkdir(parents=True, exist_ok=True)
|
|
284
|
-
out.write_text(json.dumps(result, indent=2), encoding="utf-8")
|
|
285
|
-
print(f"✅ Harvest scan exported to {out}")
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if __name__ == "__main__":
|
|
289
|
-
main()
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: epistemic_search
|
|
3
|
-
description: High-integrity web search and document fetch skill with automatic SHA-256 content caching. Use when searching the web, retrieving primary sources, or fetching documentation while enforcing epistemic integrity.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Epistemic Search & Verifiable Source Fetching
|
|
7
|
-
|
|
8
|
-
This skill provides zero-API-key web search (DuckDuckGo Lite) and content-addressed source fetching.
|
|
9
|
-
Every fetched web page is automatically saved with its SHA-256 fingerprint into `.research/sources/<sha256>.md` for mathematical auditability.
|
|
10
|
-
|
|
11
|
-
## 1. Web Search (No API Key Required)
|
|
12
|
-
|
|
13
|
-
Run search with a query:
|
|
14
|
-
```bash
|
|
15
|
-
python3 skills/epistemic_search/scripts/search.py "YOUR SEARCH QUERY"
|
|
16
|
-
```
|
|
17
|
-
Or with JSON piping:
|
|
18
|
-
```bash
|
|
19
|
-
echo '{"query": "zk-SNARK hardware benchmarks", "allowed_domains": ["arxiv.org", "eprint.iacr.org"]}' | python3 skills/epistemic_search/scripts/search.py
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Outputs `<search_results>` XML containing title, URL, and snippets.
|
|
23
|
-
|
|
24
|
-
### Anti-bot "blocked" detection (distinct from no results)
|
|
25
|
-
|
|
26
|
-
DuckDuckGo Lite sometimes answers with an HTTP-2xx image-selection CAPTCHA (the
|
|
27
|
-
"anomaly" interstitial) instead of results. It is **not** an empty result set,
|
|
28
|
-
so the script detects it and reports it distinctly:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
# Typed status + actionable hint (status: ok | empty | blocked | error)
|
|
32
|
-
python3 skills/epistemic_search/scripts/search.py --status "YOUR QUERY"
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
* `blocked` — DDG served its anti-bot page. Surface the hint, back off, and
|
|
36
|
-
retry later, or switch engines (`.research/config.json` → `search_engine`:
|
|
37
|
-
`firecrawl` / `searxng`), or export a BYO provider key
|
|
38
|
-
(`EXA_API_KEY` / `FIRECRAWL_API_KEY` / `PARALLEL_API_KEY` / `TAVILY_API_KEY` /
|
|
39
|
-
`TINYFISH_API_KEY`).
|
|
40
|
-
* `empty` — a genuine no-hits query.
|
|
41
|
-
* `error` — network/HTTP failure.
|
|
42
|
-
|
|
43
|
-
The default list contract is unchanged: `search_duckduckgo(...)` still returns a
|
|
44
|
-
list and returns `[]` when blocked (writing the hint to stderr). Programmatic
|
|
45
|
-
callers that must act on a block can use `search_duckduckgo_detailed(...)`
|
|
46
|
-
(status dict) or `require_search_duckduckgo(...)`, which raises `SearchBlocked`.
|
|
47
|
-
Every query through our path is recorded in `.research/retrieval.jsonl` with
|
|
48
|
-
its `status` and `provider`, so a block is no longer indistinguishable from a
|
|
49
|
-
zero-hit query — including the metered rungs (exa/firecrawl/parallel/tavily/
|
|
50
|
-
tinyfish/searxng/console) executed by the OpenCode `websearch` provider, so a
|
|
51
|
-
metered-only run cannot read as `0 searches (~$0.00 est)`.
|
|
52
|
-
|
|
53
|
-
The preflight reachability probe is deliberately **not** logged (it is not an
|
|
54
|
-
agent search), so a zero-search run reports `our-path searches: 0`.
|
|
55
|
-
|
|
56
|
-
## 2. Verifiable Web Fetch & Automatic Caching
|
|
57
|
-
|
|
58
|
-
Fetch any web page or documentation:
|
|
59
|
-
```bash
|
|
60
|
-
python3 skills/epistemic_search/scripts/fetch.py "https://example.com/paper.html"
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
The script outputs clean markdown and automatically persists the raw source into `.research/sources/<sha256>.md`.
|
|
64
|
-
|
|
65
|
-
Use the resulting SHA-256 hash when making claims:
|
|
66
|
-
`[VERIFIED: <first_16_chars_of_hash>]`
|
|
67
|
-
Ensure any cited quotes are exact verbatim substrings from the cached document.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Epistemic Search module
|
|
Binary file
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Zero-API-Key Web Content Fetcher with Automatic Content-Addressed SHA-256 Caching.
|
|
4
|
-
Fetches web content, cleans HTML to readable Markdown, and persists directly into
|
|
5
|
-
.research/sources/<sha256>.md for epistemic auditability.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import sys
|
|
9
|
-
import os
|
|
10
|
-
import re
|
|
11
|
-
import json
|
|
12
|
-
import urllib.request
|
|
13
|
-
import urllib.parse
|
|
14
|
-
from html.parser import HTMLParser
|
|
15
|
-
|
|
16
|
-
# Add project root to sys.path to import SourceHasher
|
|
17
|
-
PKG_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
|
18
|
-
if PKG_ROOT not in sys.path:
|
|
19
|
-
sys.path.insert(0, PKG_ROOT)
|
|
20
|
-
|
|
21
|
-
try:
|
|
22
|
-
from skills.research_cache.hasher import SourceHasher
|
|
23
|
-
except ImportError:
|
|
24
|
-
SourceHasher = None
|
|
25
|
-
|
|
26
|
-
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
27
|
-
|
|
28
|
-
class HTMLToMarkdownExtractor(HTMLParser):
|
|
29
|
-
def __init__(self):
|
|
30
|
-
super().__init__()
|
|
31
|
-
self.in_script_or_style = False
|
|
32
|
-
self.title = ""
|
|
33
|
-
self.in_title = False
|
|
34
|
-
self.text_chunks = []
|
|
35
|
-
self.headings = []
|
|
36
|
-
|
|
37
|
-
def handle_starttag(self, tag, attrs):
|
|
38
|
-
if tag in ("script", "style", "noscript", "svg", "header", "footer", "nav"):
|
|
39
|
-
self.in_script_or_style = True
|
|
40
|
-
elif tag == "title":
|
|
41
|
-
self.in_title = True
|
|
42
|
-
elif tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
|
43
|
-
self.text_chunks.append(f"\n\n{'#' * int(tag[1])} ")
|
|
44
|
-
elif tag in ("p", "div", "article", "section"):
|
|
45
|
-
self.text_chunks.append("\n\n")
|
|
46
|
-
elif tag == "li":
|
|
47
|
-
self.text_chunks.append("\n- ")
|
|
48
|
-
elif tag == "br":
|
|
49
|
-
self.text_chunks.append("\n")
|
|
50
|
-
|
|
51
|
-
def handle_endtag(self, tag):
|
|
52
|
-
if tag in ("script", "style", "noscript", "svg", "header", "footer", "nav"):
|
|
53
|
-
self.in_script_or_style = False
|
|
54
|
-
elif tag == "title":
|
|
55
|
-
self.in_title = False
|
|
56
|
-
elif tag in ("p", "div", "article", "section"):
|
|
57
|
-
self.text_chunks.append("\n")
|
|
58
|
-
|
|
59
|
-
def handle_data(self, data):
|
|
60
|
-
if self.in_script_or_style:
|
|
61
|
-
return
|
|
62
|
-
if self.in_title:
|
|
63
|
-
self.title += data
|
|
64
|
-
else:
|
|
65
|
-
self.text_chunks.append(data)
|
|
66
|
-
|
|
67
|
-
def get_markdown(self) -> str:
|
|
68
|
-
raw_text = "".join(self.text_chunks)
|
|
69
|
-
# Clean multiple blank lines
|
|
70
|
-
clean_text = re.sub(r"\n{3,}", "\n\n", raw_text).strip()
|
|
71
|
-
return clean_text
|
|
72
|
-
|
|
73
|
-
def fetch_and_cache(url: str, research_dir: str = ".research") -> str:
|
|
74
|
-
headers = {
|
|
75
|
-
"User-Agent": USER_AGENT,
|
|
76
|
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
77
|
-
"Accept-Language": "en-US,en;q=0.5",
|
|
78
|
-
}
|
|
79
|
-
req = urllib.request.Request(url, headers=headers)
|
|
80
|
-
try:
|
|
81
|
-
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
82
|
-
raw_bytes = resp.read()
|
|
83
|
-
html = raw_bytes.decode("utf-8", errors="replace")
|
|
84
|
-
except Exception as e:
|
|
85
|
-
sys.stderr.write(f"[epistemic-fetch] Error fetching URL {url}: {e}\n")
|
|
86
|
-
return f"Error fetching {url}: {e}"
|
|
87
|
-
|
|
88
|
-
parser = HTMLToMarkdownExtractor()
|
|
89
|
-
parser.feed(html)
|
|
90
|
-
page_title = parser.title.strip() or url
|
|
91
|
-
body_markdown = parser.get_markdown()
|
|
92
|
-
|
|
93
|
-
# Cache into .research/sources/<sha256>.md
|
|
94
|
-
sha256_hash = "uncached"
|
|
95
|
-
cache_file = "not saved"
|
|
96
|
-
if SourceHasher:
|
|
97
|
-
from pathlib import Path
|
|
98
|
-
hasher = SourceHasher(base_dir=Path(research_dir))
|
|
99
|
-
sha256_hash = hasher.store_source(
|
|
100
|
-
url=url,
|
|
101
|
-
title=page_title,
|
|
102
|
-
content=body_markdown
|
|
103
|
-
)
|
|
104
|
-
cache_file = str(hasher.sources_dir / f"{sha256_hash}.md")
|
|
105
|
-
else:
|
|
106
|
-
import hashlib
|
|
107
|
-
sha256_hash = hashlib.sha256(body_markdown.encode("utf-8")).hexdigest()
|
|
108
|
-
|
|
109
|
-
output = []
|
|
110
|
-
output.append(f"<!-- EPISTEMIC_SOURCE_HASH: {sha256_hash} -->")
|
|
111
|
-
output.append(f"<!-- CACHED_AT: {cache_file} -->")
|
|
112
|
-
output.append(f"# {page_title}\n")
|
|
113
|
-
output.append(f"**Source URL:** {url}")
|
|
114
|
-
output.append(f"**Content SHA-256:** `{sha256_hash}`")
|
|
115
|
-
output.append(f"**Verification Tag:** `[VERIFIED: {sha256_hash[:16]}]`\n")
|
|
116
|
-
output.append(body_markdown)
|
|
117
|
-
|
|
118
|
-
return "\n".join(output)
|
|
119
|
-
|
|
120
|
-
def main():
|
|
121
|
-
url = ""
|
|
122
|
-
research_dir = ".research"
|
|
123
|
-
|
|
124
|
-
if not sys.stdin.isatty():
|
|
125
|
-
try:
|
|
126
|
-
stdin_data = sys.stdin.read().strip()
|
|
127
|
-
if stdin_data:
|
|
128
|
-
try:
|
|
129
|
-
payload = json.loads(stdin_data)
|
|
130
|
-
url = payload.get("url", "")
|
|
131
|
-
research_dir = payload.get("research_dir", research_dir)
|
|
132
|
-
except json.JSONDecodeError:
|
|
133
|
-
url = stdin_data
|
|
134
|
-
except Exception:
|
|
135
|
-
pass
|
|
136
|
-
|
|
137
|
-
args = sys.argv[1:]
|
|
138
|
-
idx = 0
|
|
139
|
-
while idx < len(args):
|
|
140
|
-
arg = args[idx]
|
|
141
|
-
if arg == "--dir" and idx + 1 < len(args):
|
|
142
|
-
research_dir = args[idx + 1]
|
|
143
|
-
idx += 1
|
|
144
|
-
elif not url and not arg.startswith("--"):
|
|
145
|
-
url = arg
|
|
146
|
-
idx += 1
|
|
147
|
-
|
|
148
|
-
if not url:
|
|
149
|
-
sys.stderr.write("Usage: fetch.py [options] <url>\nOr pipe JSON: echo '{\"url\":\"https://...\"}' | fetch.py\n")
|
|
150
|
-
sys.exit(1)
|
|
151
|
-
|
|
152
|
-
result = fetch_and_cache(url=url, research_dir=research_dir)
|
|
153
|
-
print(result)
|
|
154
|
-
|
|
155
|
-
if __name__ == "__main__":
|
|
156
|
-
main()
|