mindforge-cc 11.8.2 → 11.9.0
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/.agent/mindforge/wf-orchestrate-review.md +35 -0
- package/.agent/mindforge/wf-verification-loop.md +45 -0
- package/.agent/mindforge/wf-workflow-optimizer.md +51 -0
- package/.agent/skills/article-illustrator/SKILL.md +7 -7
- package/.agent/skills/arxiv/SKILL.md +6 -6
- package/.agent/skills/code-wiki/SKILL.md +11 -11
- package/.agent/skills/concept-diagrams/SKILL.md +6 -6
- package/.agent/skills/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
- package/.agent/skills/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
- package/.agent/skills/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
- package/.agent/skills/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
- package/.agent/skills/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
- package/.agent/skills/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
- package/.agent/skills/concept-diagrams/examples/electricity-grid-flow.md +182 -0
- package/.agent/skills/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
- package/.agent/skills/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
- package/.agent/skills/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
- package/.agent/skills/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
- package/.agent/skills/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
- package/.agent/skills/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
- package/.agent/skills/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
- package/.agent/skills/concept-diagrams/examples/wind-turbine-structure.md +338 -0
- package/.agent/skills/concept-diagrams/templates/template.html +174 -0
- package/.agent/skills/domain-intel/SKILL.md +7 -7
- package/.agent/skills/duckduckgo-search/SKILL.md +17 -17
- package/.agent/skills/github-auth/scripts/gh-env.sh +66 -0
- package/.agent/skills/github-code-review/SKILL.md +9 -9
- package/.agent/skills/github-pr-workflow/SKILL.md +3 -3
- package/.agent/skills/godmode/SKILL.md +8 -8
- package/.agent/skills/jupyter-live-kernel/SKILL.md +3 -3
- package/.agent/skills/kanban-orchestrator/SKILL.md +10 -10
- package/.agent/skills/kanban-worker/SKILL.md +6 -6
- package/.agent/skills/llm-wiki/SKILL.md +7 -7
- package/.agent/skills/mindforge-autonomous/SKILL.md +1 -1
- package/.agent/skills/mindforge-complete-milestone/SKILL.md +1 -1
- package/.agent/skills/mindforge-discuss-phase/SKILL.md +1 -1
- package/.agent/skills/mindforge-do/SKILL.md +1 -1
- package/.agent/skills/mindforge-execute-phase/SKILL.md +1 -1
- package/.agent/skills/mindforge-list-workspaces/SKILL.md +1 -1
- package/.agent/skills/mindforge-manager/SKILL.md +1 -1
- package/.agent/skills/mindforge-neural-orchestrator/SKILL.md +1 -1
- package/.agent/skills/mindforge-new-milestone/SKILL.md +4 -4
- package/.agent/skills/mindforge-new-project/SKILL.md +4 -4
- package/.agent/skills/mindforge-new-workspace/SKILL.md +1 -1
- package/.agent/skills/mindforge-note/SKILL.md +1 -1
- package/.agent/skills/mindforge-plan-phase/SKILL.md +1 -1
- package/.agent/skills/mindforge-profile-user/SKILL.md +1 -1
- package/.agent/skills/mindforge-remove-workspace/SKILL.md +1 -1
- package/.agent/skills/mindforge-research-phase/SKILL.md +1 -1
- package/.agent/skills/mindforge-ui-phase/SKILL.md +1 -1
- package/.agent/skills/mindforge-ui-review/SKILL.md +1 -1
- package/.agent/skills/mindforge-verify-work/SKILL.md +1 -1
- package/.agent/skills/mindforge-workspace-isolated/SKILL.md +1 -1
- package/.agent/skills/node-inspect-debugger/SKILL.md +1 -1
- package/.agent/skills/obsidian/SKILL.md +15 -15
- package/.agent/skills/osint-investigation/SKILL.md +3 -2
- package/.agent/skills/osint-investigation/references/sources/courtlistener.md +98 -0
- package/.agent/skills/osint-investigation/references/sources/gdelt.md +104 -0
- package/.agent/skills/osint-investigation/references/sources/icij-offshore.md +104 -0
- package/.agent/skills/osint-investigation/references/sources/nyc-acris.md +90 -0
- package/.agent/skills/osint-investigation/references/sources/ofac-sdn.md +92 -0
- package/.agent/skills/osint-investigation/references/sources/opencorporates.md +103 -0
- package/.agent/skills/osint-investigation/references/sources/sec-edgar.md +83 -0
- package/.agent/skills/osint-investigation/references/sources/senate-ld.md +89 -0
- package/.agent/skills/osint-investigation/references/sources/usaspending.md +97 -0
- package/.agent/skills/osint-investigation/references/sources/wayback.md +93 -0
- package/.agent/skills/osint-investigation/references/sources/wikipedia.md +107 -0
- package/.agent/skills/osint-investigation/scripts/_http.py +82 -0
- package/.agent/skills/osint-investigation/scripts/_normalize.py +67 -0
- package/.agent/skills/osint-investigation/scripts/build_findings.py +221 -0
- package/.agent/skills/osint-investigation/scripts/entity_resolution.py +228 -0
- package/.agent/skills/osint-investigation/scripts/fetch_courtlistener.py +149 -0
- package/.agent/skills/osint-investigation/scripts/fetch_gdelt.py +161 -0
- package/.agent/skills/osint-investigation/scripts/fetch_icij_offshore.py +234 -0
- package/.agent/skills/osint-investigation/scripts/fetch_nyc_acris.py +203 -0
- package/.agent/skills/osint-investigation/scripts/fetch_ofac_sdn.py +175 -0
- package/.agent/skills/osint-investigation/scripts/fetch_opencorporates.py +191 -0
- package/.agent/skills/osint-investigation/scripts/fetch_sec_edgar.py +184 -0
- package/.agent/skills/osint-investigation/scripts/fetch_senate_ld.py +146 -0
- package/.agent/skills/osint-investigation/scripts/fetch_usaspending.py +170 -0
- package/.agent/skills/osint-investigation/scripts/fetch_wayback.py +142 -0
- package/.agent/skills/osint-investigation/scripts/fetch_wikipedia.py +266 -0
- package/.agent/skills/osint-investigation/scripts/timing_analysis.py +252 -0
- package/.agent/skills/oss-forensics/SKILL.md +10 -9
- package/.agent/skills/oss-forensics/scripts/evidence-store.py +313 -0
- package/.agent/skills/parallel-cli/SKILL.md +2 -2
- package/.agent/skills/pinggy-tunnel/SKILL.md +3 -3
- package/.agent/skills/pixel-art/SKILL.md +2 -2
- package/.agent/skills/plan/SKILL.md +10 -10
- package/.agent/skills/requesting-code-review/SKILL.md +4 -6
- package/.agent/skills/research-paper-writing/SKILL.md +32 -32
- package/.agent/skills/rest-graphql-debug/SKILL.md +31 -32
- package/.agent/skills/s6-container-supervision/SKILL.md +1 -1
- package/.agent/skills/scrapling/SKILL.md +1 -1
- package/.agent/skills/sherlock/SKILL.md +1 -1
- package/.agent/skills/simplify-code/SKILL.md +5 -7
- package/.agent/skills/skill-authoring/SKILL.md +4 -4
- package/.agent/skills/spike/SKILL.md +11 -13
- package/.agent/skills/subagent-driven-development/SKILL.md +7 -11
- package/.agent/skills/systematic-debugging/SKILL.md +13 -14
- package/.agent/skills/test-driven-development/SKILL.md +6 -7
- package/.agent/skills/video-orchestrator/SKILL.md +1 -1
- package/.agent/skills/web-pentest/SKILL.md +7 -7
- package/.mindforge/config.json +3 -2
- package/.mindforge/dynamic-workflows/REGISTRY.md +4 -1
- package/.mindforge/dynamic-workflows/index.json +910 -840
- package/.mindforge/dynamic-workflows/scripts/accessibility-audit.js +88 -90
- package/.mindforge/dynamic-workflows/scripts/ai-model-eval.js +59 -61
- package/.mindforge/dynamic-workflows/scripts/api-contract-test.js +87 -85
- package/.mindforge/dynamic-workflows/scripts/api-migration.js +120 -122
- package/.mindforge/dynamic-workflows/scripts/architecture-modernization.js +84 -86
- package/.mindforge/dynamic-workflows/scripts/code-audit.js +75 -77
- package/.mindforge/dynamic-workflows/scripts/code-explainer.js +99 -101
- package/.mindforge/dynamic-workflows/scripts/competitive-analysis.js +62 -64
- package/.mindforge/dynamic-workflows/scripts/competitive-teardown.js +106 -108
- package/.mindforge/dynamic-workflows/scripts/cost-analysis.js +78 -80
- package/.mindforge/dynamic-workflows/scripts/data-pipeline-validate.js +47 -49
- package/.mindforge/dynamic-workflows/scripts/database-migration.js +85 -87
- package/.mindforge/dynamic-workflows/scripts/debug-detective.js +97 -99
- package/.mindforge/dynamic-workflows/scripts/dependency-health.js +81 -83
- package/.mindforge/dynamic-workflows/scripts/design-system-audit.js +84 -86
- package/.mindforge/dynamic-workflows/scripts/documentation-gen.js +65 -67
- package/.mindforge/dynamic-workflows/scripts/feature-planner.js +78 -80
- package/.mindforge/dynamic-workflows/scripts/incident-response.js +81 -83
- package/.mindforge/dynamic-workflows/scripts/multi-repo-sync.js +49 -51
- package/.mindforge/dynamic-workflows/scripts/mutation-testing.js +113 -115
- package/.mindforge/dynamic-workflows/scripts/onboard-codebase.js +79 -81
- package/.mindforge/dynamic-workflows/scripts/orchestrate-review.js +115 -0
- package/.mindforge/dynamic-workflows/scripts/perf-optimize.js +97 -99
- package/.mindforge/dynamic-workflows/scripts/pr-review.js +61 -63
- package/.mindforge/dynamic-workflows/scripts/refactor-plan.js +88 -90
- package/.mindforge/dynamic-workflows/scripts/release-prep.js +79 -81
- package/.mindforge/dynamic-workflows/scripts/security-hardening.js +117 -119
- package/.mindforge/dynamic-workflows/scripts/security-threat-model.js +119 -121
- package/.mindforge/dynamic-workflows/scripts/tdd-sprint.js +80 -82
- package/.mindforge/dynamic-workflows/scripts/tech-evaluation.js +50 -52
- package/.mindforge/dynamic-workflows/scripts/test-coverage-gap.js +68 -70
- package/.mindforge/dynamic-workflows/scripts/ux-heuristic-audit.js +89 -91
- package/.mindforge/dynamic-workflows/scripts/verification-loop.js +138 -0
- package/.mindforge/dynamic-workflows/scripts/workflow-optimizer.js +136 -0
- package/.mindforge/dynamic-workflows/scripts/writer-reviewer.js +63 -65
- package/.mindforge/memory/sync-manifest.json +1 -1
- package/.mindforge/personas/advisor-researcher.md +2 -2
- package/.mindforge/personas/api-load-tester.md +1 -1
- package/.mindforge/personas/authentication-architect.md +1 -1
- package/.mindforge/personas/backup-recovery-specialist.md +1 -1
- package/.mindforge/personas/build-optimizer.md +1 -1
- package/.mindforge/personas/caching-strategist.md +1 -1
- package/.mindforge/personas/chaos-engineer.md +1 -1
- package/.mindforge/personas/compliance-auditor.md +1 -1
- package/.mindforge/personas/config-management-expert.md +1 -1
- package/.mindforge/personas/coverage-specialist.md +1 -1
- package/.mindforge/personas/debug-specialist.md +2 -2
- package/.mindforge/personas/debugger.md +2 -2
- package/.mindforge/personas/dependency-auditor.md +1 -1
- package/.mindforge/personas/developer.md +1 -1
- package/.mindforge/personas/devops-engineer.md +1 -1
- package/.mindforge/personas/executor.md +1 -1
- package/.mindforge/personas/go-specialist.md +1 -1
- package/.mindforge/personas/incident-commander.md +1 -1
- package/.mindforge/personas/java-specialist.md +1 -1
- package/.mindforge/personas/kubernetes-debugger.md +1 -1
- package/.mindforge/personas/logging-architect.md +1 -1
- package/.mindforge/personas/mf-planner.md +1 -1
- package/.mindforge/personas/mf-researcher.md +1 -1
- package/.mindforge/personas/mf-reviewer.md +1 -1
- package/.mindforge/personas/nyquist-auditor.md +1 -1
- package/.mindforge/personas/observability-engineer.md +1 -1
- package/.mindforge/personas/performance-optimizer.md +1 -1
- package/.mindforge/personas/phase-researcher.md +5 -5
- package/.mindforge/personas/planner.md +1 -1
- package/.mindforge/personas/project-researcher.md +2 -2
- package/.mindforge/personas/python-specialist.md +1 -1
- package/.mindforge/personas/qa-engineer.md +1 -1
- package/.mindforge/personas/release-manager.md +1 -1
- package/.mindforge/personas/research-agent.md +4 -4
- package/.mindforge/personas/roadmapper-extend.md +1 -1
- package/.mindforge/personas/rust-specialist.md +1 -1
- package/.mindforge/personas/security-reviewer.md +1 -1
- package/.mindforge/personas/tech-writer.md +1 -1
- package/.mindforge/personas/typescript-wizard.md +1 -1
- package/.mindforge/personas/ui-researcher.md +1 -1
- package/.mindforge/skills/accessibility/SKILL.md +3 -3
- package/.mindforge/skills/agent-architecture-audit/SKILL.md +11 -0
- package/.mindforge/skills/agent-loops/SKILL.md +1 -1
- package/.mindforge/skills/arxiv/SKILL.md +6 -6
- package/.mindforge/skills/arxiv/scripts/search_arxiv.py +114 -0
- package/.mindforge/skills/code-tour/SKILL.md +2 -2
- package/.mindforge/skills/code-wiki/SKILL.md +11 -11
- package/.mindforge/skills/code-wiki/templates/README.md +31 -0
- package/.mindforge/skills/code-wiki/templates/architecture.md +30 -0
- package/.mindforge/skills/code-wiki/templates/getting-started.md +47 -0
- package/.mindforge/skills/code-wiki/templates/module.md +38 -0
- package/.mindforge/skills/concept-diagrams/SKILL.md +6 -6
- package/.mindforge/skills/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
- package/.mindforge/skills/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
- package/.mindforge/skills/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
- package/.mindforge/skills/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
- package/.mindforge/skills/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
- package/.mindforge/skills/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
- package/.mindforge/skills/concept-diagrams/examples/electricity-grid-flow.md +182 -0
- package/.mindforge/skills/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
- package/.mindforge/skills/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
- package/.mindforge/skills/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
- package/.mindforge/skills/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
- package/.mindforge/skills/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
- package/.mindforge/skills/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
- package/.mindforge/skills/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
- package/.mindforge/skills/concept-diagrams/examples/wind-turbine-structure.md +338 -0
- package/.mindforge/skills/concept-diagrams/references/dashboard-patterns.md +43 -0
- package/.mindforge/skills/concept-diagrams/references/infrastructure-patterns.md +144 -0
- package/.mindforge/skills/concept-diagrams/references/physical-shape-cookbook.md +42 -0
- package/.mindforge/skills/concept-diagrams/templates/template.html +174 -0
- package/.mindforge/skills/creative-ideation/references/full-prompt-library.md +110 -0
- package/.mindforge/skills/documentation/SKILL.md +1 -1
- package/.mindforge/skills/domain-intel/SKILL.md +7 -7
- package/.mindforge/skills/domain-intel/scripts/domain_intel.py +397 -0
- package/.mindforge/skills/duckduckgo-search/SKILL.md +17 -17
- package/.mindforge/skills/duckduckgo-search/scripts/duckduckgo.sh +28 -0
- package/.mindforge/skills/environment-management/SKILL.md +1 -1
- package/.mindforge/skills/github-code-review/SKILL.md +9 -9
- package/.mindforge/skills/github-code-review/references/review-output-template.md +74 -0
- package/.mindforge/skills/github-issues/templates/bug-report.md +35 -0
- package/.mindforge/skills/github-issues/templates/feature-request.md +31 -0
- package/.mindforge/skills/github-pr-workflow/SKILL.md +3 -3
- package/.mindforge/skills/github-pr-workflow/references/ci-troubleshooting.md +183 -0
- package/.mindforge/skills/github-pr-workflow/references/conventional-commits.md +71 -0
- package/.mindforge/skills/github-pr-workflow/templates/pr-body-bugfix.md +35 -0
- package/.mindforge/skills/github-pr-workflow/templates/pr-body-feature.md +33 -0
- package/.mindforge/skills/incident-response/SKILL.md +1 -1
- package/.mindforge/skills/jupyter-live-kernel/SKILL.md +3 -3
- package/.mindforge/skills/kanban-orchestrator/SKILL.md +10 -10
- package/.mindforge/skills/kanban-worker/SKILL.md +6 -6
- package/.mindforge/skills/meme-generation/SKILL.md +1 -1
- package/.mindforge/skills/meme-generation/scripts/generate_meme.py +470 -0
- package/.mindforge/skills/meme-generation/scripts/templates.json +97 -0
- package/.mindforge/skills/obsidian/SKILL.md +15 -15
- package/.mindforge/skills/orch-pipeline/SKILL.md +2 -2
- package/.mindforge/skills/osint-investigation/SKILL.md +3 -2
- package/.mindforge/skills/osint-investigation/references/sources/courtlistener.md +98 -0
- package/.mindforge/skills/osint-investigation/references/sources/gdelt.md +104 -0
- package/.mindforge/skills/osint-investigation/references/sources/icij-offshore.md +104 -0
- package/.mindforge/skills/osint-investigation/references/sources/nyc-acris.md +90 -0
- package/.mindforge/skills/osint-investigation/references/sources/ofac-sdn.md +92 -0
- package/.mindforge/skills/osint-investigation/references/sources/opencorporates.md +103 -0
- package/.mindforge/skills/osint-investigation/references/sources/sec-edgar.md +83 -0
- package/.mindforge/skills/osint-investigation/references/sources/senate-ld.md +89 -0
- package/.mindforge/skills/osint-investigation/references/sources/usaspending.md +97 -0
- package/.mindforge/skills/osint-investigation/references/sources/wayback.md +93 -0
- package/.mindforge/skills/osint-investigation/references/sources/wikipedia.md +107 -0
- package/.mindforge/skills/osint-investigation/scripts/_http.py +82 -0
- package/.mindforge/skills/osint-investigation/scripts/_normalize.py +67 -0
- package/.mindforge/skills/osint-investigation/scripts/build_findings.py +221 -0
- package/.mindforge/skills/osint-investigation/scripts/entity_resolution.py +228 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_courtlistener.py +149 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_gdelt.py +161 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_icij_offshore.py +234 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_nyc_acris.py +203 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_ofac_sdn.py +175 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_opencorporates.py +191 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_sec_edgar.py +184 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_senate_ld.py +146 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_usaspending.py +170 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_wayback.py +142 -0
- package/.mindforge/skills/osint-investigation/scripts/fetch_wikipedia.py +266 -0
- package/.mindforge/skills/osint-investigation/scripts/timing_analysis.py +252 -0
- package/.mindforge/skills/osint-investigation/templates/source-template.md +59 -0
- package/.mindforge/skills/oss-forensics/SKILL.md +6 -6
- package/.mindforge/skills/oss-forensics/references/evidence-types.md +89 -0
- package/.mindforge/skills/oss-forensics/references/github-archive-guide.md +184 -0
- package/.mindforge/skills/oss-forensics/references/investigation-templates.md +131 -0
- package/.mindforge/skills/oss-forensics/references/recovery-techniques.md +164 -0
- package/.mindforge/skills/oss-forensics/scripts/evidence-store.py +313 -0
- package/.mindforge/skills/oss-forensics/templates/forensic-report.md +151 -0
- package/.mindforge/skills/oss-forensics/templates/malicious-package-report.md +43 -0
- package/.mindforge/skills/pixel-art/SKILL.md +2 -2
- package/.mindforge/skills/pixel-art/references/palettes.md +49 -0
- package/.mindforge/skills/plan/SKILL.md +10 -10
- package/.mindforge/skills/requesting-code-review/SKILL.md +4 -6
- package/.mindforge/skills/research-paper-writing/SKILL.md +32 -32
- package/.mindforge/skills/research-paper-writing/references/autoreason-methodology.md +394 -0
- package/.mindforge/skills/research-paper-writing/references/checklists.md +434 -0
- package/.mindforge/skills/research-paper-writing/references/citation-workflow.md +563 -0
- package/.mindforge/skills/research-paper-writing/references/experiment-patterns.md +728 -0
- package/.mindforge/skills/research-paper-writing/references/human-evaluation.md +476 -0
- package/.mindforge/skills/research-paper-writing/references/paper-types.md +481 -0
- package/.mindforge/skills/research-paper-writing/references/reviewer-guidelines.md +433 -0
- package/.mindforge/skills/research-paper-writing/references/sources.md +191 -0
- package/.mindforge/skills/research-paper-writing/references/writing-guide.md +474 -0
- package/.mindforge/skills/research-paper-writing/templates/README.md +251 -0
- package/.mindforge/skills/scrapling/SKILL.md +1 -1
- package/.mindforge/skills/security-review/SKILL.md +1 -1
- package/.mindforge/skills/sherlock/SKILL.md +1 -1
- package/.mindforge/skills/simplify-code/SKILL.md +5 -7
- package/.mindforge/skills/spike/SKILL.md +11 -13
- package/.mindforge/skills/subagent-driven-development/SKILL.md +7 -11
- package/.mindforge/skills/subagent-driven-development/references/context-budget-discipline.md +53 -0
- package/.mindforge/skills/subagent-driven-development/references/gates-taxonomy.md +93 -0
- package/.mindforge/skills/systematic-debugging/SKILL.md +13 -14
- package/.mindforge/skills/test-driven-development/SKILL.md +7 -8
- package/.mindforge/skills/testing-standards/SKILL.md +1 -1
- package/.mindforge/skills/web-pentest/SKILL.md +2 -2
- package/.mindforge/skills/web-pentest/references/bypass-techniques.md +133 -0
- package/.mindforge/skills/web-pentest/references/exploitation-techniques.md +204 -0
- package/.mindforge/skills/web-pentest/references/scope-enforcement.md +110 -0
- package/.mindforge/skills/web-pentest/references/vuln-taxonomy.md +81 -0
- package/.mindforge/skills/web-pentest/templates/authorization.md +69 -0
- package/.mindforge/skills/web-pentest/templates/pentest-report.md +178 -0
- package/.mindforge/skills/writing-skills/SKILL.md +4 -3
- package/CHANGELOG.md +35 -0
- package/MINDFORGE.md +3 -3
- package/README.md +9 -7
- package/RELEASENOTES.md +65 -2
- package/SECURITY.md +5 -3
- package/bin/autonomous/auto-runner.js +5 -2
- package/bin/engine/skill-loader.js +4 -0
- package/bin/governance/rbac.js +4 -0
- package/bin/memory/eis-client.js +1 -0
- package/bin/mindforge-cli.js +5 -0
- package/bin/models/bedrock-provider.js +192 -0
- package/bin/models/model-client.js +19 -0
- package/bin/spawn-agent.js +9 -2
- package/docs/commands-reference.md +68 -6
- package/docs/faq.md +38 -1
- package/docs/getting-started.md +27 -12
- package/docs/sdk-reference.md +36 -3
- package/docs/troubleshooting.md +59 -1
- package/docs/user-guide.md +7 -4
- package/package.json +3 -3
- package/.claude/commands/mindforge/skill-tdd.md +0 -53
- package/.claude/commands/mindforge/skills-index.md +0 -118
- package/.claude/commands/mindforge/systematic-debug.md +0 -60
- package/.claude/commands/mindforge/wf-accessibility-audit.md +0 -31
- package/.claude/commands/mindforge/wf-ai-model-eval.md +0 -31
- package/.claude/commands/mindforge/wf-api-migration.md +0 -31
- package/.claude/commands/mindforge/wf-architecture-modernization.md +0 -32
- package/.claude/commands/mindforge/wf-catalog.md +0 -59
- package/.claude/commands/mindforge/wf-code-audit.md +0 -31
- package/.claude/commands/mindforge/wf-code-explainer.md +0 -31
- package/.claude/commands/mindforge/wf-competitive-analysis.md +0 -31
- package/.claude/commands/mindforge/wf-competitive-teardown.md +0 -31
- package/.claude/commands/mindforge/wf-cost-analysis.md +0 -31
- package/.claude/commands/mindforge/wf-data-pipeline-validate.md +0 -31
- package/.claude/commands/mindforge/wf-database-migration.md +0 -31
- package/.claude/commands/mindforge/wf-debug-detective.md +0 -32
- package/.claude/commands/mindforge/wf-design-system-audit.md +0 -31
- package/.claude/commands/mindforge/wf-documentation-gen.md +0 -31
- package/.claude/commands/mindforge/wf-feature-planner.md +0 -31
- package/.claude/commands/mindforge/wf-incident-response.md +0 -31
- package/.claude/commands/mindforge/wf-multi-repo-sync.md +0 -31
- package/.claude/commands/mindforge/wf-mutation-testing.md +0 -31
- package/.claude/commands/mindforge/wf-onboard-codebase.md +0 -31
- package/.claude/commands/mindforge/wf-perf-optimize.md +0 -31
- package/.claude/commands/mindforge/wf-pr-review.md +0 -31
- package/.claude/commands/mindforge/wf-refactor-plan.md +0 -31
- package/.claude/commands/mindforge/wf-release-prep.md +0 -31
- package/.claude/commands/mindforge/wf-security-threat-model.md +0 -31
- package/.claude/commands/mindforge/wf-tdd-sprint.md +0 -31
- package/.claude/commands/mindforge/wf-tech-evaluation.md +0 -31
- package/.claude/commands/mindforge/wf-ux-heuristic-audit.md +0 -31
- package/.claude/commands/mindforge/wf-writer-reviewer.md +0 -30
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Search the GDELT 2.0 DOC API for news mentions.
|
|
3
|
+
|
|
4
|
+
GDELT monitors world news in 100+ languages and indexes the full text.
|
|
5
|
+
Free, anonymous, ~15-minute update frequency. Covers ~2015→present.
|
|
6
|
+
|
|
7
|
+
Useful for surfacing news mentions of a person, company, or topic across
|
|
8
|
+
international media — much wider net than Google News.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import csv
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import urllib.parse
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
20
|
+
from _http import get_json # noqa: E402
|
|
21
|
+
|
|
22
|
+
BASE = "https://api.gdeltproject.org/api/v2/doc/doc"
|
|
23
|
+
|
|
24
|
+
COLUMNS = [
|
|
25
|
+
"title",
|
|
26
|
+
"url",
|
|
27
|
+
"seen_date",
|
|
28
|
+
"domain",
|
|
29
|
+
"language",
|
|
30
|
+
"source_country",
|
|
31
|
+
"tone",
|
|
32
|
+
"social_image",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fetch(
|
|
37
|
+
query: str,
|
|
38
|
+
mode: str,
|
|
39
|
+
timespan: str | None,
|
|
40
|
+
start_datetime: str | None,
|
|
41
|
+
end_datetime: str | None,
|
|
42
|
+
source_country: str | None,
|
|
43
|
+
source_lang: str | None,
|
|
44
|
+
limit: int,
|
|
45
|
+
out_path: str,
|
|
46
|
+
) -> int:
|
|
47
|
+
params: dict[str, str] = {
|
|
48
|
+
"query": query,
|
|
49
|
+
"mode": mode,
|
|
50
|
+
"format": "json",
|
|
51
|
+
"maxrecords": str(min(limit, 250)),
|
|
52
|
+
"sort": "datedesc",
|
|
53
|
+
}
|
|
54
|
+
if timespan:
|
|
55
|
+
params["timespan"] = timespan
|
|
56
|
+
if start_datetime:
|
|
57
|
+
params["startdatetime"] = start_datetime.replace("-", "").replace(":", "").replace(" ", "")
|
|
58
|
+
if end_datetime:
|
|
59
|
+
params["enddatetime"] = end_datetime.replace("-", "").replace(":", "").replace(" ", "")
|
|
60
|
+
if source_country:
|
|
61
|
+
params["sourcecountry"] = source_country
|
|
62
|
+
if source_lang:
|
|
63
|
+
params["sourcelang"] = source_lang
|
|
64
|
+
|
|
65
|
+
url = f"{BASE}?{urllib.parse.urlencode(params)}"
|
|
66
|
+
payload: dict | list = {}
|
|
67
|
+
for attempt in range(3):
|
|
68
|
+
try:
|
|
69
|
+
payload = get_json(url)
|
|
70
|
+
break
|
|
71
|
+
except RuntimeError as e:
|
|
72
|
+
# GDELT requires 1 request per 5 seconds; back off and retry.
|
|
73
|
+
if "429" in str(e) and attempt < 2:
|
|
74
|
+
print(
|
|
75
|
+
f"GDELT throttle hit; sleeping 6s before retry "
|
|
76
|
+
f"(attempt {attempt + 1}/3)",
|
|
77
|
+
file=sys.stderr,
|
|
78
|
+
)
|
|
79
|
+
time.sleep(6)
|
|
80
|
+
continue
|
|
81
|
+
print(f"GDELT error: {e}", file=sys.stderr)
|
|
82
|
+
payload = {}
|
|
83
|
+
break
|
|
84
|
+
except Exception as e: # noqa: BLE001
|
|
85
|
+
print(f"GDELT error: {e}", file=sys.stderr)
|
|
86
|
+
payload = {}
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
rows: list[dict[str, str]] = []
|
|
90
|
+
if isinstance(payload, dict):
|
|
91
|
+
articles = payload.get("articles", []) or []
|
|
92
|
+
for a in articles[:limit]:
|
|
93
|
+
seen = (a.get("seendate") or "")
|
|
94
|
+
# GDELT format: 20260319T083000Z → 2026-03-19 08:30:00Z
|
|
95
|
+
if len(seen) == 16 and "T" in seen:
|
|
96
|
+
seen = f"{seen[0:4]}-{seen[4:6]}-{seen[6:8]} {seen[9:11]}:{seen[11:13]}:{seen[13:15]}Z"
|
|
97
|
+
rows.append(
|
|
98
|
+
{
|
|
99
|
+
"title": (a.get("title") or "").replace("\n", " ").strip(),
|
|
100
|
+
"url": a.get("url") or "",
|
|
101
|
+
"seen_date": seen,
|
|
102
|
+
"domain": a.get("domain") or "",
|
|
103
|
+
"language": a.get("language") or "",
|
|
104
|
+
"source_country": a.get("sourcecountry") or "",
|
|
105
|
+
"tone": str(a.get("tone") or ""),
|
|
106
|
+
"social_image": a.get("socialimage") or "",
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
with open(out_path, "w", newline="", encoding="utf-8") as fh:
|
|
112
|
+
w = csv.DictWriter(fh, fieldnames=COLUMNS)
|
|
113
|
+
w.writeheader()
|
|
114
|
+
w.writerows(rows)
|
|
115
|
+
if not rows:
|
|
116
|
+
print(
|
|
117
|
+
f"GDELT: 0 articles for query={query!r}. "
|
|
118
|
+
"GDELT indexes ~2015→present. Try widening the timespan or "
|
|
119
|
+
"checking the query syntax (https://blog.gdeltproject.org/gdelt-doc-2-0-api-debuts/).",
|
|
120
|
+
file=sys.stderr,
|
|
121
|
+
)
|
|
122
|
+
return len(rows)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> int:
|
|
126
|
+
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
127
|
+
p.add_argument("--query", required=True, help='Search query (supports GDELT operators: quoted phrases, AND/OR/NOT, sourcecountry:, theme:)')
|
|
128
|
+
p.add_argument(
|
|
129
|
+
"--mode",
|
|
130
|
+
default="ArtList",
|
|
131
|
+
choices=["ArtList", "ImageCollage", "TimelineVol", "TimelineTone", "ToneChart"],
|
|
132
|
+
help="GDELT mode (default ArtList for article list)",
|
|
133
|
+
)
|
|
134
|
+
p.add_argument(
|
|
135
|
+
"--timespan",
|
|
136
|
+
help="Relative window: e.g. '1d', '1w', '1m', '3m', '1y' (overrides start/end)",
|
|
137
|
+
)
|
|
138
|
+
p.add_argument("--start", help="Absolute start YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS")
|
|
139
|
+
p.add_argument("--end", help="Absolute end YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS")
|
|
140
|
+
p.add_argument("--source-country", help="2-letter source country (e.g. US, UK)")
|
|
141
|
+
p.add_argument("--source-lang", help="Source language (e.g. English, Spanish)")
|
|
142
|
+
p.add_argument("--limit", type=int, default=100)
|
|
143
|
+
p.add_argument("--out", required=True)
|
|
144
|
+
a = p.parse_args()
|
|
145
|
+
n = fetch(
|
|
146
|
+
query=a.query,
|
|
147
|
+
mode=a.mode,
|
|
148
|
+
timespan=a.timespan,
|
|
149
|
+
start_datetime=a.start,
|
|
150
|
+
end_datetime=a.end,
|
|
151
|
+
source_country=a.source_country,
|
|
152
|
+
source_lang=a.source_lang,
|
|
153
|
+
limit=a.limit,
|
|
154
|
+
out_path=a.out,
|
|
155
|
+
)
|
|
156
|
+
print(f"Wrote {n} GDELT article rows to {a.out}")
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Search ICIJ Offshore Leaks via the bulk CSV database.
|
|
3
|
+
|
|
4
|
+
The old reconcile endpoint (https://offshoreleaks.icij.org/reconcile) returns
|
|
5
|
+
404 — ICIJ has removed it. The remaining stable access path is the public
|
|
6
|
+
bulk download:
|
|
7
|
+
|
|
8
|
+
https://offshoreleaks-data.icij.org/offshoreleaks/csv/full-oldb.LATEST.zip
|
|
9
|
+
|
|
10
|
+
~70 MB, ~6 CSVs inside (nodes-entities, nodes-officers, nodes-intermediaries,
|
|
11
|
+
nodes-addresses, relationships, ...). We cache it under
|
|
12
|
+
$HERMES_OSINT_CACHE/icij/ (default: ~/.cache/hermes-osint/icij/) and search
|
|
13
|
+
locally so the agent doesn't re-download for every query.
|
|
14
|
+
|
|
15
|
+
Output CSV columns match the original `fetch_icij_offshore.py` contract.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import csv
|
|
21
|
+
import io
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
import urllib.request
|
|
27
|
+
import zipfile
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
BULK_URL = "https://offshoreleaks-data.icij.org/offshoreleaks/csv/full-oldb.LATEST.zip"
|
|
31
|
+
|
|
32
|
+
COLUMNS = [
|
|
33
|
+
"node_id",
|
|
34
|
+
"name",
|
|
35
|
+
"node_type",
|
|
36
|
+
"country_codes",
|
|
37
|
+
"countries",
|
|
38
|
+
"jurisdiction",
|
|
39
|
+
"incorporation_date",
|
|
40
|
+
"inactivation_date",
|
|
41
|
+
"source",
|
|
42
|
+
"entity_url",
|
|
43
|
+
"connections",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cache_dir() -> Path:
|
|
48
|
+
base = os.environ.get("HERMES_OSINT_CACHE")
|
|
49
|
+
if base:
|
|
50
|
+
return Path(base) / "icij"
|
|
51
|
+
return Path.home() / ".cache" / "hermes-osint" / "icij"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _download(dest: Path, force: bool = False) -> Path:
|
|
55
|
+
"""Download (or reuse cached) ICIJ bulk ZIP."""
|
|
56
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
zip_path = dest / "full-oldb.zip"
|
|
58
|
+
if zip_path.exists() and not force:
|
|
59
|
+
# Re-check age: refetch if older than 30 days.
|
|
60
|
+
age_days = (time.time() - zip_path.stat().st_mtime) / 86400
|
|
61
|
+
if age_days < 30:
|
|
62
|
+
return zip_path
|
|
63
|
+
print(f"Downloading ICIJ bulk database (~70 MB) to {zip_path}", file=sys.stderr)
|
|
64
|
+
req = urllib.request.Request(
|
|
65
|
+
BULK_URL,
|
|
66
|
+
headers={"User-Agent": "hermes-agent osint-investigation skill"},
|
|
67
|
+
)
|
|
68
|
+
with urllib.request.urlopen(req, timeout=120) as resp: # noqa: S310
|
|
69
|
+
tmp = zip_path.with_suffix(".zip.tmp")
|
|
70
|
+
with open(tmp, "wb") as fh:
|
|
71
|
+
while True:
|
|
72
|
+
chunk = resp.read(1 << 16)
|
|
73
|
+
if not chunk:
|
|
74
|
+
break
|
|
75
|
+
fh.write(chunk)
|
|
76
|
+
tmp.replace(zip_path)
|
|
77
|
+
return zip_path
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _open_csv(zf: zipfile.ZipFile, name_pattern: str):
|
|
81
|
+
"""Open the first CSV matching name_pattern (case-insensitive substring)."""
|
|
82
|
+
for info in zf.infolist():
|
|
83
|
+
if name_pattern.lower() in info.filename.lower() and info.filename.lower().endswith(".csv"):
|
|
84
|
+
return zf.open(info), info.filename
|
|
85
|
+
return None, None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _match(needle_norm: str, hay: str) -> bool:
|
|
89
|
+
return needle_norm in (hay or "").upper()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _normalize_query(s: str) -> str:
|
|
93
|
+
s = s.upper()
|
|
94
|
+
s = re.sub(r"[^\w\s]", " ", s)
|
|
95
|
+
s = re.sub(r"\s+", " ", s).strip()
|
|
96
|
+
return s
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def fetch(
|
|
100
|
+
entity: str | None,
|
|
101
|
+
officer: str | None,
|
|
102
|
+
jurisdiction: str | None,
|
|
103
|
+
out_path: str,
|
|
104
|
+
cache_dir: Path,
|
|
105
|
+
force_refresh: bool = False,
|
|
106
|
+
limit: int = 500,
|
|
107
|
+
) -> int:
|
|
108
|
+
zip_path = _download(cache_dir, force=force_refresh)
|
|
109
|
+
rows: list[dict[str, str]] = []
|
|
110
|
+
needles: list[tuple[str, str]] = [] # (kind, normalized needle)
|
|
111
|
+
if entity:
|
|
112
|
+
needles.append(("Entity", _normalize_query(entity)))
|
|
113
|
+
if officer:
|
|
114
|
+
needles.append(("Officer", _normalize_query(officer)))
|
|
115
|
+
jur_norm = _normalize_query(jurisdiction) if jurisdiction else None
|
|
116
|
+
|
|
117
|
+
targets = [
|
|
118
|
+
("Entity", "nodes-entities"),
|
|
119
|
+
("Officer", "nodes-officers"),
|
|
120
|
+
("Intermediary", "nodes-intermediaries"),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
124
|
+
for node_type, csv_substring in targets:
|
|
125
|
+
relevant_needles = [n for (k, n) in needles if k in {node_type, "Entity", "Officer"}] or []
|
|
126
|
+
# Only scan a CSV if we have a needle that could plausibly match it,
|
|
127
|
+
# or if we have ONLY a jurisdiction filter.
|
|
128
|
+
applicable_needles = [n for (k, n) in needles if k == node_type]
|
|
129
|
+
if needles and not applicable_needles and not jur_norm:
|
|
130
|
+
continue
|
|
131
|
+
stream, fname = _open_csv(zf, csv_substring)
|
|
132
|
+
if not stream:
|
|
133
|
+
continue
|
|
134
|
+
with stream:
|
|
135
|
+
text = io.TextIOWrapper(stream, encoding="utf-8", errors="replace")
|
|
136
|
+
reader = csv.DictReader(text)
|
|
137
|
+
for row in reader:
|
|
138
|
+
name = (row.get("name") or "").strip()
|
|
139
|
+
if not name:
|
|
140
|
+
continue
|
|
141
|
+
name_u = name.upper()
|
|
142
|
+
matched = False
|
|
143
|
+
for n in applicable_needles or relevant_needles:
|
|
144
|
+
if _match(n, name_u):
|
|
145
|
+
matched = True
|
|
146
|
+
break
|
|
147
|
+
if not needles:
|
|
148
|
+
matched = True # jurisdiction-only sweep
|
|
149
|
+
if not matched:
|
|
150
|
+
continue
|
|
151
|
+
jur = (row.get("jurisdiction_description") or row.get("country_codes") or "").strip()
|
|
152
|
+
if jur_norm and jur_norm not in jur.upper() and jur_norm not in (row.get("countries") or "").upper():
|
|
153
|
+
continue
|
|
154
|
+
node_id = (row.get("node_id") or "").strip()
|
|
155
|
+
rows.append(
|
|
156
|
+
{
|
|
157
|
+
"node_id": node_id,
|
|
158
|
+
"name": name,
|
|
159
|
+
"node_type": node_type,
|
|
160
|
+
"country_codes": row.get("country_codes", "") or "",
|
|
161
|
+
"countries": row.get("countries", "") or "",
|
|
162
|
+
"jurisdiction": jur,
|
|
163
|
+
"incorporation_date": row.get("incorporation_date", "") or "",
|
|
164
|
+
"inactivation_date": row.get("inactivation_date", "") or "",
|
|
165
|
+
"source": row.get("sourceID", "") or row.get("source", "") or "",
|
|
166
|
+
"entity_url": (
|
|
167
|
+
f"https://offshoreleaks.icij.org/nodes/{node_id}" if node_id else ""
|
|
168
|
+
),
|
|
169
|
+
"connections": "",
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
if len(rows) >= limit:
|
|
173
|
+
break
|
|
174
|
+
if len(rows) >= limit:
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
with open(out_path, "w", newline="", encoding="utf-8") as fh:
|
|
179
|
+
w = csv.DictWriter(fh, fieldnames=COLUMNS)
|
|
180
|
+
w.writeheader()
|
|
181
|
+
w.writerows(rows)
|
|
182
|
+
if not rows:
|
|
183
|
+
bits = []
|
|
184
|
+
if entity:
|
|
185
|
+
bits.append(f"entity={entity!r}")
|
|
186
|
+
if officer:
|
|
187
|
+
bits.append(f"officer={officer!r}")
|
|
188
|
+
if jurisdiction:
|
|
189
|
+
bits.append(f"jurisdiction={jurisdiction!r}")
|
|
190
|
+
print(
|
|
191
|
+
f"ICIJ: 0 matches for {', '.join(bits)}. "
|
|
192
|
+
"The bulk database covers offshore leaks (Panama, Paradise, Pandora, "
|
|
193
|
+
"Bahamas, Offshore Leaks). Most private US individuals are NOT in it.",
|
|
194
|
+
file=sys.stderr,
|
|
195
|
+
)
|
|
196
|
+
return len(rows)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main() -> int:
|
|
200
|
+
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
201
|
+
p.add_argument("--entity", help="Search by entity name (substring, case-insensitive)")
|
|
202
|
+
p.add_argument("--officer", help="Search by officer / individual name (substring, case-insensitive)")
|
|
203
|
+
p.add_argument("--jurisdiction", help="Filter results by jurisdiction substring")
|
|
204
|
+
p.add_argument("--limit", type=int, default=500)
|
|
205
|
+
p.add_argument("--out", required=True)
|
|
206
|
+
p.add_argument(
|
|
207
|
+
"--cache-dir",
|
|
208
|
+
type=Path,
|
|
209
|
+
default=None,
|
|
210
|
+
help="Override cache directory (default: $HERMES_OSINT_CACHE/icij or ~/.cache/hermes-osint/icij)",
|
|
211
|
+
)
|
|
212
|
+
p.add_argument(
|
|
213
|
+
"--force-refresh",
|
|
214
|
+
action="store_true",
|
|
215
|
+
help="Re-download the bulk ZIP even if a recent cached copy exists.",
|
|
216
|
+
)
|
|
217
|
+
a = p.parse_args()
|
|
218
|
+
if not (a.entity or a.officer or a.jurisdiction):
|
|
219
|
+
p.error("must supply at least one of --entity / --officer / --jurisdiction")
|
|
220
|
+
n = fetch(
|
|
221
|
+
entity=a.entity,
|
|
222
|
+
officer=a.officer,
|
|
223
|
+
jurisdiction=a.jurisdiction,
|
|
224
|
+
out_path=a.out,
|
|
225
|
+
cache_dir=a.cache_dir or _cache_dir(),
|
|
226
|
+
force_refresh=a.force_refresh,
|
|
227
|
+
limit=a.limit,
|
|
228
|
+
)
|
|
229
|
+
print(f"Wrote {n} ICIJ Offshore Leaks rows to {a.out}")
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Search NYC property records via ACRIS (Automated City Register Information System).
|
|
3
|
+
|
|
4
|
+
Uses the city's Socrata-backed open data API. No auth required for read access.
|
|
5
|
+
|
|
6
|
+
Datasets:
|
|
7
|
+
bnx9-e6tj — Real Property Master (one row per recorded document)
|
|
8
|
+
636b-3b5g — Real Property Parties (names — grantor, grantee, etc.)
|
|
9
|
+
8h5j-fqxa — Real Property Legal (lot / property identifiers)
|
|
10
|
+
uqqa-hym2 — Real Property References
|
|
11
|
+
|
|
12
|
+
The Parties dataset has the names. We search by name and optionally join to
|
|
13
|
+
Master to get the doc type and date.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import csv
|
|
19
|
+
import sys
|
|
20
|
+
import urllib.parse
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
24
|
+
from _http import get_json # noqa: E402
|
|
25
|
+
|
|
26
|
+
PARTIES_URL = "https://data.cityofnewyork.us/resource/636b-3b5g.json"
|
|
27
|
+
MASTER_URL = "https://data.cityofnewyork.us/resource/bnx9-e6tj.json"
|
|
28
|
+
|
|
29
|
+
PARTY_TYPE = {
|
|
30
|
+
"1": "grantor (seller / mortgagor / debtor)",
|
|
31
|
+
"2": "grantee (buyer / mortgagee / creditor)",
|
|
32
|
+
"3": "other party",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
BOROUGH = {
|
|
36
|
+
"1": "Manhattan",
|
|
37
|
+
"2": "Bronx",
|
|
38
|
+
"3": "Brooklyn",
|
|
39
|
+
"4": "Queens",
|
|
40
|
+
"5": "Staten Island",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
COLUMNS = [
|
|
44
|
+
"document_id",
|
|
45
|
+
"name",
|
|
46
|
+
"party_type",
|
|
47
|
+
"party_role",
|
|
48
|
+
"address_1",
|
|
49
|
+
"address_2",
|
|
50
|
+
"city",
|
|
51
|
+
"state",
|
|
52
|
+
"zip",
|
|
53
|
+
"country",
|
|
54
|
+
"doc_type",
|
|
55
|
+
"doc_date",
|
|
56
|
+
"recorded_date",
|
|
57
|
+
"borough",
|
|
58
|
+
"amount",
|
|
59
|
+
"filing_url",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _filing_url(document_id: str) -> str:
|
|
64
|
+
if not document_id:
|
|
65
|
+
return ""
|
|
66
|
+
return (
|
|
67
|
+
f"https://a836-acris.nyc.gov/DS/DocumentSearch/DocumentImageView?doc_id={document_id}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def fetch(
|
|
72
|
+
name: str | None,
|
|
73
|
+
address: str | None,
|
|
74
|
+
party_type: str | None,
|
|
75
|
+
limit: int,
|
|
76
|
+
out_path: str,
|
|
77
|
+
enrich: bool = True,
|
|
78
|
+
) -> int:
|
|
79
|
+
if not (name or address):
|
|
80
|
+
raise SystemExit("must supply --name or --address")
|
|
81
|
+
|
|
82
|
+
where_clauses: list[str] = []
|
|
83
|
+
if name:
|
|
84
|
+
safe = name.upper().replace("'", "''")
|
|
85
|
+
where_clauses.append(f"upper(name) like '%{safe}%'")
|
|
86
|
+
if address:
|
|
87
|
+
safe_addr = address.upper().replace("'", "''")
|
|
88
|
+
where_clauses.append(f"upper(address_1) like '%{safe_addr}%'")
|
|
89
|
+
if party_type and party_type in {"1", "2", "3"}:
|
|
90
|
+
where_clauses.append(f"party_type='{party_type}'")
|
|
91
|
+
|
|
92
|
+
params = {
|
|
93
|
+
"$where": " AND ".join(where_clauses),
|
|
94
|
+
"$limit": str(limit),
|
|
95
|
+
}
|
|
96
|
+
url = f"{PARTIES_URL}?{urllib.parse.urlencode(params)}"
|
|
97
|
+
parties = get_json(url)
|
|
98
|
+
if not isinstance(parties, list):
|
|
99
|
+
raise SystemExit(f"Unexpected ACRIS response: {parties!r}")
|
|
100
|
+
|
|
101
|
+
# Enrich with master record (doc_type, dates, borough, amount).
|
|
102
|
+
doc_ids: list[str] = sorted({
|
|
103
|
+
d for d in (p.get("document_id") for p in parties) if d
|
|
104
|
+
})
|
|
105
|
+
masters: dict[str, dict] = {}
|
|
106
|
+
if enrich and doc_ids:
|
|
107
|
+
# Batch up to 100 doc_ids per request (Socrata IN-list is fine for this).
|
|
108
|
+
for i in range(0, len(doc_ids), 100):
|
|
109
|
+
chunk = doc_ids[i : i + 100]
|
|
110
|
+
id_list = ",".join(f"'{d}'" for d in chunk)
|
|
111
|
+
master_params = {
|
|
112
|
+
"$where": f"document_id in ({id_list})",
|
|
113
|
+
"$limit": "100",
|
|
114
|
+
}
|
|
115
|
+
url = f"{MASTER_URL}?{urllib.parse.urlencode(master_params)}"
|
|
116
|
+
try:
|
|
117
|
+
rows = get_json(url)
|
|
118
|
+
except Exception as e: # noqa: BLE001
|
|
119
|
+
print(f"ACRIS master lookup failed for chunk: {e}", file=sys.stderr)
|
|
120
|
+
continue
|
|
121
|
+
if isinstance(rows, list):
|
|
122
|
+
for r in rows:
|
|
123
|
+
did = r.get("document_id", "")
|
|
124
|
+
if did:
|
|
125
|
+
masters[did] = r
|
|
126
|
+
|
|
127
|
+
out_rows: list[dict[str, str]] = []
|
|
128
|
+
for p in parties:
|
|
129
|
+
did = p.get("document_id", "") or ""
|
|
130
|
+
m = masters.get(did, {})
|
|
131
|
+
out_rows.append(
|
|
132
|
+
{
|
|
133
|
+
"document_id": did,
|
|
134
|
+
"name": p.get("name", "") or "",
|
|
135
|
+
"party_type": p.get("party_type", "") or "",
|
|
136
|
+
"party_role": PARTY_TYPE.get(p.get("party_type", ""), ""),
|
|
137
|
+
"address_1": p.get("address_1", "") or "",
|
|
138
|
+
"address_2": p.get("address_2", "") or "",
|
|
139
|
+
"city": p.get("city", "") or "",
|
|
140
|
+
"state": p.get("state", "") or "",
|
|
141
|
+
"zip": p.get("zip", "") or "",
|
|
142
|
+
"country": p.get("country", "") or "",
|
|
143
|
+
"doc_type": m.get("doc_type", "") or "",
|
|
144
|
+
"doc_date": (m.get("document_date", "") or "")[:10],
|
|
145
|
+
"recorded_date": (m.get("recorded_datetime", "") or "")[:10],
|
|
146
|
+
"borough": BOROUGH.get(m.get("recorded_borough", ""), m.get("recorded_borough", "")),
|
|
147
|
+
"amount": m.get("document_amt", "") or "",
|
|
148
|
+
"filing_url": _filing_url(did),
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
with open(out_path, "w", newline="", encoding="utf-8") as fh:
|
|
154
|
+
w = csv.DictWriter(fh, fieldnames=COLUMNS)
|
|
155
|
+
w.writeheader()
|
|
156
|
+
w.writerows(out_rows)
|
|
157
|
+
|
|
158
|
+
if not out_rows:
|
|
159
|
+
filters = []
|
|
160
|
+
if name:
|
|
161
|
+
filters.append(f"name={name!r}")
|
|
162
|
+
if address:
|
|
163
|
+
filters.append(f"address={address!r}")
|
|
164
|
+
print(
|
|
165
|
+
f"NYC ACRIS: 0 records for {', '.join(filters)}. "
|
|
166
|
+
"ACRIS covers ONLY NYC (5 boroughs). For property records elsewhere, "
|
|
167
|
+
"search the relevant county recorder directly.",
|
|
168
|
+
file=sys.stderr,
|
|
169
|
+
)
|
|
170
|
+
return len(out_rows)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def main() -> int:
|
|
174
|
+
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
175
|
+
p.add_argument("--name", help="Party name substring (case-insensitive)")
|
|
176
|
+
p.add_argument("--address", help="Address line 1 substring")
|
|
177
|
+
p.add_argument(
|
|
178
|
+
"--party-type",
|
|
179
|
+
choices=["1", "2", "3"],
|
|
180
|
+
help="Filter party type: 1=grantor (seller/mortgagor), 2=grantee (buyer/mortgagee), 3=other",
|
|
181
|
+
)
|
|
182
|
+
p.add_argument("--limit", type=int, default=200)
|
|
183
|
+
p.add_argument(
|
|
184
|
+
"--no-enrich",
|
|
185
|
+
action="store_true",
|
|
186
|
+
help="Skip the master-document lookup that adds doc_type/date/amount",
|
|
187
|
+
)
|
|
188
|
+
p.add_argument("--out", required=True)
|
|
189
|
+
a = p.parse_args()
|
|
190
|
+
n = fetch(
|
|
191
|
+
name=a.name,
|
|
192
|
+
address=a.address,
|
|
193
|
+
party_type=a.party_type,
|
|
194
|
+
limit=a.limit,
|
|
195
|
+
out_path=a.out,
|
|
196
|
+
enrich=not a.no_enrich,
|
|
197
|
+
)
|
|
198
|
+
print(f"Wrote {n} NYC ACRIS rows to {a.out}")
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
raise SystemExit(main())
|