argus-code 0.4.3__tar.gz → 0.4.4__tar.gz
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.
- argus_code-0.4.4/AGENTS.md +119 -0
- argus_code-0.4.4/CLAUDE.md +14 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/PKG-INFO +1 -1
- {argus_code-0.4.3 → argus_code-0.4.4}/SECURITY.md +22 -7
- argus_code-0.4.4/dashboard/AGENTS.md +35 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/tools.astro +5 -1
- argus_code-0.4.4/dashboard-dist/_astro/tools.astro_astro_type_script_index_0_lang.DHTUv2RL.js +12 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/tools/index.html +1 -1
- {argus_code-0.4.3 → argus_code-0.4.4}/pyproject.toml +1 -1
- argus_code-0.4.4/python/argus/AGENTS.md +50 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/__init__.py +1 -1
- argus_code-0.4.4/python/argus/adapters/AGENTS.md +44 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/adapter.py +14 -3
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/discover.py +14 -1
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/cli.py +24 -1
- argus_code-0.4.4/python/argus/collector/AGENTS.md +37 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/process.py +6 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/scaffold/scaffolder.py +29 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/scaffold/snapshot.py +42 -2
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/scaffold/storage.py +17 -1
- argus_code-0.4.4/python/argus/server/AGENTS.md +46 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/server/api.py +36 -12
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/server/app.py +58 -1
- argus_code-0.4.4/python/argus/store/AGENTS.md +43 -0
- argus_code-0.4.4/tests/AGENTS.md +35 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_discover.py +5 -5
- argus_code-0.4.4/tests/adapters/claude_code/test_path_containment.py +154 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/cli/test_indexing_cli.py +47 -47
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_process.py +3 -1
- argus_code-0.4.4/tests/daemon/test_spawn_safe_path.py +64 -0
- argus_code-0.4.4/tests/dashboard/test_chart_tooltip_escaping.py +87 -0
- argus_code-0.4.4/tests/scaffold/test_scaffold_containment.py +157 -0
- argus_code-0.4.4/tests/scaffold/test_snapshot_secrets.py +103 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/server/test_api.py +11 -7
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/server/test_api_search.py +5 -1
- argus_code-0.4.4/tests/server/test_param_bounds.py +99 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/server/test_server.py +153 -6
- argus_code-0.4.4/tests/store/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/test_e2e.py +5 -1
- {argus_code-0.4.3 → argus_code-0.4.4}/uv.lock +4 -4
- argus_code-0.4.3/dashboard-dist/_astro/tools.astro_astro_type_script_index_0_lang.OvyHH6MH.js +0 -12
- {argus_code-0.4.3 → argus_code-0.4.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/.github/pull_request_template.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/.gitignore +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/.npmrc +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/ARCHITECTURE.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/CONTRIBUTING.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/LICENSE +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/NOTICE +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/PRD.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/README.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/TESTING.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/.astro/content-assets.mjs +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/.astro/content-modules.mjs +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/.astro/content.d.ts +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/.astro/types.d.ts +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/astro.config.mjs +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/package-lock.json +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/package.json +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/public/styles/global.css +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/layouts/Default.astro +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/index.astro +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/models.astro +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/prompts.astro +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/session.astro +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/sessions/index.astro +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/settings.astro +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/trends.astro +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/scripts/alerts.ts +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/scripts/api.ts +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/scripts/charts.ts +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/scripts/format.ts +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/styles/global.css +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/tsconfig.json +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/charts.36uaSAWt.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/format.BexCUztC.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.BF0nZzma.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.D-QKQJAF.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/installCanvasRenderer.XK5yP7fW.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/models.astro_astro_type_script_index_0_lang.0Aczy7Vm.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/prompts.astro_astro_type_script_index_0_lang.B1i5N4zM.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/session.CGJ-XRoC.css +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/session.astro_astro_type_script_index_0_lang.SRsknMTL.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/settings.astro_astro_type_script_index_0_lang.DyHh8NQi.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/trends.astro_astro_type_script_index_0_lang.CBtUcxGK.js +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/index.html +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/models/index.html +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/prompts/index.html +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/session/index.html +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/sessions/index.html +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/settings/index.html +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/styles/global.css +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/trends/index.html +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/pricing/2026-05-02.json +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/pricing/2026-06-12.json +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/base.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/extract_tool_calls.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/extract_transcript.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/extract_turns.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/history_jsonl.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/ingest_file.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/model.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/schemas.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/registry.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/aggregate.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/first_run.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/pipeline.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/rollup_subagents.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/scheduler.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/search_backfill.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/watcher.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/core/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/core/runtime.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/logging.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/pidfile.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/service.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/detectors/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/detectors/base.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/detectors/registry.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/detectors/tool_error_rate_spike.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/compute.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/load.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/refresh.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/types.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/scaffold/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/schema/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/schema/types.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/server/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/db.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/migrations/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/migrations/inline.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/repository.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/agents/code-reviewer.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/agents/security-auditor.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/commit.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/deploy.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/fix-issue.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/pr.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/review.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/rules/api-conventions.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/rules/code-style.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/rules/testing.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/settings.json +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/skills/example/SKILL.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/CLAUDE.md +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_adapter.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_extract_tool_calls.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_extract_transcript.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_extract_turns.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_history_jsonl.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_ingest_file.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_integration.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_model.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_schemas.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/test_registry.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/cli/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_aggregate.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_first_run.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_pipeline.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_rollup_subagents.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_scheduler.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_watcher.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/conftest.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/core/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/core/test_runtime.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_logs.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_pidfile.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_service.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_yield.py +0 -0
- {argus_code-0.4.3/tests/detectors → argus_code-0.4.4/tests/dashboard}/__init__.py +0 -0
- {argus_code-0.4.3/tests/pricing → argus_code-0.4.4/tests/detectors}/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/detectors/test_registry.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/detectors/test_tool_error_rate_spike.py +0 -0
- {argus_code-0.4.3/tests/scaffold → argus_code-0.4.4/tests/pricing}/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/pricing/test_compute.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/pricing/test_load.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/pricing/test_refresh.py +0 -0
- {argus_code-0.4.3/tests/schema → argus_code-0.4.4/tests/scaffold}/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_bundled_template.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_cli_claude.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_scaffolder.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_snapshot.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_storage.py +0 -0
- {argus_code-0.4.3/tests/server → argus_code-0.4.4/tests/schema}/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/schema/test_types.py +0 -0
- {argus_code-0.4.3/tests/store → argus_code-0.4.4/tests/server}/__init__.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/server/test_week_of.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/store/test_db.py +0 -0
- {argus_code-0.4.3 → argus_code-0.4.4}/tests/store/test_repository.py +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# AGENTS.md — Argus (argus-code) root work contract
|
|
2
|
+
|
|
3
|
+
> This repository uses the **DOX framework**: AGENTS.md files are binding work
|
|
4
|
+
> contracts for their subtree. Before editing any path, read this file and every
|
|
5
|
+
> AGENTS.md on the route from the repo root down to that path. The nearest doc
|
|
6
|
+
> controls local detail; this root controls repo-wide rules. No child doc may
|
|
7
|
+
> weaken DOX. Re-read the chain in-session; do not rely on memory.
|
|
8
|
+
>
|
|
9
|
+
> **After a meaningful change, run the DOX pass:** update the nearest owning
|
|
10
|
+
> AGENTS.md (and any affected parent/child index) when you change purpose, scope,
|
|
11
|
+
> contracts, workflows, inputs/outputs, or the doc tree itself; delete stale text;
|
|
12
|
+
> then re-check changed paths against the chain. Pure no-behavior edits may leave
|
|
13
|
+
> docs unchanged, but the pass still happens.
|
|
14
|
+
|
|
15
|
+
## Purpose
|
|
16
|
+
|
|
17
|
+
Argus is a **local-first** analytics dashboard for Claude Code (and other coding
|
|
18
|
+
agents). A watcher ingests Claude Code's `.jsonl` session transcripts into a local
|
|
19
|
+
SQLite archive (`~/.argus/argus.db`); a FastAPI server serves a static Astro
|
|
20
|
+
dashboard at `http://localhost:4242`. No network, no telemetry, no LLM calls
|
|
21
|
+
(except opt-in `argus pricing refresh`). Backend + dashboard ship together in the
|
|
22
|
+
`argus-code` wheel on PyPI.
|
|
23
|
+
|
|
24
|
+
Orientation docs (prose, not contracts — read but don't duplicate into AGENTS.md):
|
|
25
|
+
- `ARCHITECTURE.md` — how the pieces fit together (read before adding a feature).
|
|
26
|
+
- `CONTRIBUTING.md` — dev quick start (`uv sync`, `uv run argus start`).
|
|
27
|
+
- `TESTING.md` — test catalog and conventions.
|
|
28
|
+
|
|
29
|
+
## Ownership
|
|
30
|
+
|
|
31
|
+
This root owns: the development workflow below, global engineering rules, the
|
|
32
|
+
release process, and the top-level Child DOX Index. Code-level contracts live in
|
|
33
|
+
the nearest child AGENTS.md.
|
|
34
|
+
|
|
35
|
+
## Local Contracts
|
|
36
|
+
|
|
37
|
+
### Development workflow (feature / bug → PR → merge → release → tag → publish)
|
|
38
|
+
|
|
39
|
+
Follow this end-to-end for every feature or bug fix.
|
|
40
|
+
|
|
41
|
+
1. **Branch.** Never commit feature/bug work directly to `main`. Branch from an
|
|
42
|
+
up-to-date `main`: `fix/<slug>` for bugs, `feat/<slug>` for features.
|
|
43
|
+
2. **Implement test-first.** Reproduce the bug (or specify the feature) with a
|
|
44
|
+
failing test, then make it pass. See `tests/AGENTS.md`.
|
|
45
|
+
3. **Commit** in small, logical units. Conventional-commit subjects
|
|
46
|
+
(`fix:`, `feat:`, `test:`, `release:`, `docs:`). End every commit message with:
|
|
47
|
+
```
|
|
48
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
49
|
+
```
|
|
50
|
+
4. **Verify** before opening the PR — see Verification below. Tests must be green
|
|
51
|
+
(the one known Windows flake is the scheduler timing test; confirm it passes in
|
|
52
|
+
isolation).
|
|
53
|
+
5. **Push & open PR** against `main`: `gh pr create --base main --head <branch>`.
|
|
54
|
+
End the PR body with:
|
|
55
|
+
```
|
|
56
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
57
|
+
```
|
|
58
|
+
6. **Merge** with squash and delete the branch:
|
|
59
|
+
`gh pr merge <n> --squash --delete-branch`. Then sync local `main`.
|
|
60
|
+
7. **Release** (only when cutting a version — see Release process below).
|
|
61
|
+
8. **Tag + GitHub release:** `gh release create vX.Y.Z --target main --title vX.Y.Z --notes "..."`.
|
|
62
|
+
9. **PyPI publish is the maintainer's manual step** — see Release process.
|
|
63
|
+
|
|
64
|
+
### Release process (manual — no CI release, no stored credentials)
|
|
65
|
+
|
|
66
|
+
When cutting version `X.Y.Z`:
|
|
67
|
+
1. Bump the version in **both** files with the Edit tool (not PowerShell
|
|
68
|
+
`Set-Content` — it injects a BOM): `pyproject.toml` (`version = "X.Y.Z"`) and
|
|
69
|
+
`python/argus/__init__.py` (`__version__ = "X.Y.Z"`).
|
|
70
|
+
2. `uv lock` to sync `uv.lock` to the new version.
|
|
71
|
+
3. If the dashboard changed, rebuild and refresh the shipped copy — see
|
|
72
|
+
`dashboard/AGENTS.md`.
|
|
73
|
+
4. Clean-build the artifacts: **`rm -rf dist && uv build`** (always clear `dist/`
|
|
74
|
+
first, or stale files get uploaded).
|
|
75
|
+
5. Confirm the wheel ships the right version + dashboard before publishing.
|
|
76
|
+
6. Tag + GitHub release (workflow step 8).
|
|
77
|
+
7. **Publish to PyPI — maintainer only:** `uv publish --token pypi-<token>`. The
|
|
78
|
+
token is the maintainer's secret; **an agent must never run this or handle the
|
|
79
|
+
token.** Stop and hand off.
|
|
80
|
+
|
|
81
|
+
## Work Guidance
|
|
82
|
+
|
|
83
|
+
Global engineering rules (binding):
|
|
84
|
+
- **Never destroy user data in `~/.argus/argus.db`.** Migrations must be
|
|
85
|
+
non-destructive, idempotent, and self-healing. See `python/argus/store/AGENTS.md`.
|
|
86
|
+
- **No hacks to make a test pass.** Fix the real cause, or correct a stale test
|
|
87
|
+
expectation to match the real contract (and say so). This is a prod package used
|
|
88
|
+
by many; never hardcode a value just to go green.
|
|
89
|
+
- **Backend runs from source** (`uv run argus start`) — no build step for Python.
|
|
90
|
+
Only the dashboard has a build step.
|
|
91
|
+
- Plans/specs under `docs/superpowers/` are scratch and gitignored — do not commit them.
|
|
92
|
+
|
|
93
|
+
## Verification
|
|
94
|
+
|
|
95
|
+
- Backend: `uv run pytest` (must be green; ~290 tests, ~25s on Windows).
|
|
96
|
+
- Dashboard: `cd dashboard && npm run build` (must succeed).
|
|
97
|
+
- Built form smoke test: see `CONTRIBUTING.md` "Running the built form".
|
|
98
|
+
|
|
99
|
+
## User Preferences
|
|
100
|
+
|
|
101
|
+
- Releases are cut deliberately by the maintainer; agents prepare everything up to
|
|
102
|
+
(and including the GitHub tag/release when asked) but never run `uv publish`.
|
|
103
|
+
- Prefer honest reporting over green-at-any-cost: surface flakes, skips, and
|
|
104
|
+
pre-existing failures explicitly.
|
|
105
|
+
|
|
106
|
+
## Child DOX Index
|
|
107
|
+
|
|
108
|
+
- `python/argus/AGENTS.md` — Python backend package; owns the subsystems without
|
|
109
|
+
their own doc (`core`, `daemon`, `detectors`, `pricing`, `scaffold`, `schema`)
|
|
110
|
+
and indexes the four below.
|
|
111
|
+
- `python/argus/store/AGENTS.md` — SQLite, migrations, repository; data-safety
|
|
112
|
+
and path-normalization contracts.
|
|
113
|
+
- `python/argus/collector/AGENTS.md` — ingest pipeline and the missing-data
|
|
114
|
+
backfill (incl. sub-agent segment re-read).
|
|
115
|
+
- `python/argus/adapters/AGENTS.md` — Claude Code adapter and transcript-segment
|
|
116
|
+
extraction; sub-agent file layout.
|
|
117
|
+
- `python/argus/server/AGENTS.md` — FastAPI routes, static serving, clean shutdown.
|
|
118
|
+
- `dashboard/AGENTS.md` — Astro dashboard and the `dashboard-dist` ship chain.
|
|
119
|
+
- `tests/AGENTS.md` — pytest conventions and platform quirks.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This repository uses the **DOX framework** — an `AGENTS.md` hierarchy of binding
|
|
4
|
+
work contracts. `AGENTS.md` (repo root) is the source of truth, including the full
|
|
5
|
+
development workflow (branch → PR → squash-merge → release → tag → publish).
|
|
6
|
+
|
|
7
|
+
**Before editing any path:** read the root `AGENTS.md`, then every `AGENTS.md` on
|
|
8
|
+
the route down to that path; the nearest doc controls local detail. **After a
|
|
9
|
+
meaningful change:** run the DOX closeout (update the nearest owning doc and any
|
|
10
|
+
affected parent/child indexes). Re-read the chain in-session — don't rely on memory.
|
|
11
|
+
|
|
12
|
+
The root contract (imported in full below):
|
|
13
|
+
|
|
14
|
+
@AGENTS.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: argus-code
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Local-first dashboard for Claude Code cost, token, tool-usage, and full-text search analytics
|
|
5
5
|
Project-URL: Homepage, https://github.com/KrishBhimani/argus-code
|
|
6
6
|
Project-URL: Repository, https://github.com/KrishBhimani/argus-code.git
|
|
@@ -33,8 +33,10 @@ Argus is built for one user, one machine. It assumes:
|
|
|
33
33
|
before rendering in the dashboard, and never executed.
|
|
34
34
|
- **Untrusted browsers visiting localhost.** Other webpages the user
|
|
35
35
|
loads while Argus is running are not trusted. They cannot read
|
|
36
|
-
Argus data (Same-Origin Policy on responses
|
|
37
|
-
|
|
36
|
+
Argus data (Same-Origin Policy on responses, backed by a loopback
|
|
37
|
+
`Host` allowlist so DNS rebinding can't turn a hostile domain into
|
|
38
|
+
a same-origin one) and cannot trigger state changes (CSRF Origin
|
|
39
|
+
check on POST).
|
|
38
40
|
|
|
39
41
|
## Defaults that matter
|
|
40
42
|
|
|
@@ -78,14 +80,23 @@ Things this codebase deliberately does to reduce attack surface:
|
|
|
78
80
|
`escapeHtml()` before reaching `innerHTML`. The only HTML allowed
|
|
79
81
|
through is `<mark>...</mark>` from FTS5 `snippet()`, applied via a
|
|
80
82
|
separate `safeSnippet()` helper that escapes everything else.
|
|
81
|
-
- `watchdog` does not follow symlinks by default.
|
|
82
|
-
|
|
83
|
-
and
|
|
84
|
-
`~/.claude/` tree.
|
|
83
|
+
- `watchdog` does not follow symlinks by default. Every read additionally
|
|
84
|
+
goes through `ClaudeCodeAdapter.ingest_file`, which calls
|
|
85
|
+
`Path.resolve(strict=True)` and refuses anything that doesn't
|
|
86
|
+
canonicalise under the `~/.claude/` tree — symlink or NTFS junction.
|
|
87
|
+
The check sits at that single choke point (not at each caller) so
|
|
88
|
+
discovery, the watcher, and the sub-agent walk are all covered by
|
|
89
|
+
construction; `sub_agent_files_for` filters as well.
|
|
85
90
|
- Per-tick read cap of 64 MiB on session JSONL and `history.jsonl`,
|
|
86
91
|
so a runaway or hostile multi-GB file can't OOM the process.
|
|
87
92
|
- CSRF Origin check on all non-GET API routes (FastAPI middleware in
|
|
88
93
|
`python/argus/server/app.py`).
|
|
94
|
+
- Loopback `Host` header allowlist on **every** route, static mount
|
|
95
|
+
included (`is_loopback_host` in `python/argus/server/app.py`). This is
|
|
96
|
+
what stops DNS rebinding: a page on `evil.com` that repoints its own
|
|
97
|
+
DNS at `127.0.0.1` still sends `Host: evil.com` and gets a 421, so it
|
|
98
|
+
never reaches the GET routes the Origin check deliberately exempts.
|
|
99
|
+
Skipped only under `--host 0.0.0.0`, which is already a warned opt-out.
|
|
89
100
|
- No build hooks running on user machines. The wheel ships pure Python
|
|
90
101
|
+ bundled data (`dashboard-dist/`, `pricing/`, `templates/`).
|
|
91
102
|
Installation runs no arbitrary code.
|
|
@@ -95,7 +106,11 @@ Things this codebase deliberately does to reduce attack surface:
|
|
|
95
106
|
- `argus claude` scaffolding is a pure file copy — no templating,
|
|
96
107
|
substitution, or code execution. It writes only to the path you hand
|
|
97
108
|
`init` (created if absent) or under `~/.argus/templates/`, and never
|
|
98
|
-
overwrites an existing `CLAUDE.md`.
|
|
109
|
+
overwrites an existing `CLAUDE.md`. That containment is enforced, not
|
|
110
|
+
just intended: every destination must canonicalise under the target
|
|
111
|
+
directory, so a hostile repo shipping `.claude/` as a symlink or NTFS
|
|
112
|
+
junction can't redirect the write, and template names are validated as
|
|
113
|
+
plain directory names so `--template ../../x` can't escape the store.
|
|
99
114
|
|
|
100
115
|
If you spot a regression on any of the above, that's a real bug and
|
|
101
116
|
worth reporting.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# AGENTS.md — dashboard (Astro frontend + ship chain)
|
|
2
|
+
|
|
3
|
+
Parent: repo-root `AGENTS.md`. The statically-built UI served by `server/`.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Astro + ECharts dashboard. Source pages live in `src/pages/*.astro`
|
|
8
|
+
(`session.astro`, `settings.astro`, `overview`, `timeline`, …) with shared
|
|
9
|
+
scripts in `src/scripts/`.
|
|
10
|
+
|
|
11
|
+
## Local Contracts
|
|
12
|
+
|
|
13
|
+
- **Three "dist" directories — don't conflate them:**
|
|
14
|
+
- `dashboard/dist/` — raw Astro build output. **Gitignored.**
|
|
15
|
+
- `dashboard-dist/` (repo root) — the **tracked** copy that ships in the wheel
|
|
16
|
+
via `pyproject.toml` force-include (`"dashboard-dist" = "argus/dashboard-dist"`).
|
|
17
|
+
- `dist/` (repo root) — `uv build` output uploaded to PyPI. **Gitignored.**
|
|
18
|
+
- **A dashboard source edit is invisible to the package until `dashboard-dist` is
|
|
19
|
+
refreshed and committed.** After changing anything under `dashboard/src/`:
|
|
20
|
+
```sh
|
|
21
|
+
cd dashboard && npm run build && cd ..
|
|
22
|
+
git rm -r --quiet dashboard-dist && cp -r dashboard/dist dashboard-dist && git add -A dashboard-dist
|
|
23
|
+
```
|
|
24
|
+
Commit the rebuilt `dashboard-dist` alongside the source change. Astro filenames
|
|
25
|
+
are content-hashed, so a real change shows as new `_astro/*.js|css` plus updated
|
|
26
|
+
`*/index.html` — verify the new asset (and absence of stale strings) in
|
|
27
|
+
`dashboard-dist` before committing.
|
|
28
|
+
- Avoid em dashes / non-ASCII in strings that surface in the **terminal**
|
|
29
|
+
(CLI/console) — they mojibake on the Windows console. (UI text is fine.)
|
|
30
|
+
|
|
31
|
+
## Verification
|
|
32
|
+
|
|
33
|
+
`cd dashboard && npm run build` must succeed. After refreshing `dashboard-dist`,
|
|
34
|
+
optionally confirm the wheel ships it (`rm -rf dist && uv build`, then inspect the
|
|
35
|
+
`.whl`).
|
|
@@ -72,7 +72,11 @@ import Default from '../layouts/Default.astro';
|
|
|
72
72
|
const i = p[0].dataIndex;
|
|
73
73
|
const calls = values[i];
|
|
74
74
|
const err = errors?.[i] ?? 0;
|
|
75
|
-
return
|
|
75
|
+
// ECharts renders a custom tooltip formatter's return value as
|
|
76
|
+
// HTML, so the label -- a tool / MCP-server / sub-agent name read
|
|
77
|
+
// verbatim from .jsonl -- must be escaped here too, exactly like
|
|
78
|
+
// every other innerHTML sink on this page.
|
|
79
|
+
return `${escapeHtml(labels[i])}<br>Calls: ${calls.toLocaleString()}` +
|
|
76
80
|
(err ? `<br>Errors: ${err.toLocaleString()} (${((err/calls)*100).toFixed(1)}%)` : '');
|
|
77
81
|
},
|
|
78
82
|
},
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import{a as C,n,e as u}from"./format.BexCUztC.js";import{u as S,i as L,a as k,b as M,c as T,d as z,e as E}from"./installCanvasRenderer.XK5yP7fW.js";S([L,k,M,T,z]);const a=e=>document.querySelector(e),f=a("#w");let $,g,w;function h(e,t,o,l,c){const i=E(e);return i.setOption({textStyle:{color:"#9ba6b3"},tooltip:{trigger:"axis",axisPointer:{type:"shadow"},backgroundColor:"#1c222d",borderColor:"#262d3a",textStyle:{color:"#e6edf3",fontSize:12},formatter:d=>{const p=d[0].dataIndex,b=o[p],r=c?.[p]??0;return`${u(t[p])}<br>Calls: ${b.toLocaleString()}`+(r?`<br>Errors: ${r.toLocaleString()} (${(r/b*100).toFixed(1)}%)`:"")}},grid:{left:160,right:30,top:8,bottom:28},xAxis:{type:"value",axisLabel:{color:"#6b7585",fontSize:11},splitLine:{lineStyle:{color:"#1c222d"}},axisLine:{show:!1}},yAxis:{type:"category",data:t.slice().reverse(),axisLabel:{color:"#6b7585",fontSize:11},axisLine:{show:!1},axisTick:{show:!1}},series:[{type:"bar",data:o.slice().reverse(),itemStyle:{color:l,borderRadius:[0,4,4,0]},barWidth:"60%"}]}),i}function v(e,t,o,l="scalar"){const c=l==="name"?"font-size:1.05rem;font-family:'SF Mono',Consolas,monospace;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:block;color:var(--text-0);":"",i=l==="name"?`title="${t.replace(/"/g,""")}"`:"";return`<div class="card"><div class="kpi">
|
|
2
|
+
<div class="kpi-label">${e}</div>
|
|
3
|
+
<div class="kpi-value" style="${c}" ${i}>${t}</div>
|
|
4
|
+
${o?`<div class="kpi-sub">${o}</div>`:""}
|
|
5
|
+
</div></div>`}async function x(){const e=await C.toolsOverview(f.value),t=e.total_calls,o=e.total_errors,l=t>0?(o/t*100).toFixed(1)+"%":"—",c=e.tool_leaderboard[0]?.name??"—",i=e.mcp_servers[0]?.server??"—";if(a("#kpis").innerHTML=[v("Tool calls",n(t),f.value==="all"?"all time":f.value),v("Error rate",l,o>0?`${n(o)} errors`:"no errors"),v("Top tool",u(c),e.tool_leaderboard[0]?`${n(e.tool_leaderboard[0].calls)} calls`:"","name"),v("Top MCP",u(i),e.mcp_servers[0]?`${n(e.mcp_servers[0].calls)} calls`:"no MCP usage","name")].join(""),$?.dispose(),e.tool_leaderboard.length===0)a("#leaderboard").innerHTML='<p class="empty">No tool activity in this window.</p>';else{const s=e.tool_leaderboard.slice(0,12);$=h(a("#leaderboard"),s.map(m=>m.name),s.map(m=>m.calls),"#f0883e",s.map(m=>m.errors))}const d=a("#splits"),p=d.children[0],b=d.children[1],r=e.mcp_servers.length>0,y=e.subagents.length>0;g?.dispose(),w?.dispose(),d.style.display=r||y?"":"none",p.style.display=r?"":"none",b.style.display=y?"":"none",r&&(g=h(a("#mcp"),e.mcp_servers.map(s=>s.server),e.mcp_servers.map(s=>s.calls),"#58a6ff",e.mcp_servers.map(s=>s.errors))),y&&(w=h(a("#subagents"),e.subagents.map(s=>s.type),e.subagents.map(s=>s.calls),"#bc8cff",e.subagents.map(s=>s.errors)));const _=a("#tools-table tbody");_.innerHTML=e.tool_leaderboard.length===0?'<tr><td colspan="4" class="empty">—</td></tr>':e.tool_leaderboard.map(s=>`
|
|
6
|
+
<tr>
|
|
7
|
+
<td><code>${u(s.name)}</code></td>
|
|
8
|
+
<td class="num">${n(s.calls)}</td>
|
|
9
|
+
<td class="num">${n(s.errors)}</td>
|
|
10
|
+
<td class="num">${(s.error_rate*100).toFixed(1)}%</td>
|
|
11
|
+
</tr>
|
|
12
|
+
`).join("")}f.addEventListener("change",x);x();window.addEventListener("resize",()=>[$,g,w].forEach(e=>e?.resize()));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Argus — Tools</title><link rel="stylesheet" href="/styles/global.css"></head> <body> <header class="app-header"> <a href="/" class="brand">ARGUS<span class="dot">●</span></a> <nav class="app-nav"> <a href="/" class>Overview</a> <a href="/sessions" class>Sessions</a> <a href="/tools" class>Tools</a> <a href="/prompts" class>Prompts</a> <a href="/trends" class>Trends</a> <a href="/models" class>Models</a> <a href="/settings" class>Settings</a> </nav> </header> <main> <h2>Tool usage</h2> <div class="card" style="margin-bottom:1rem;display:flex;gap:0.6rem;align-items:center;"> <label style="color:var(--text-2);font-size:0.8rem;">Window</label> <select id="w"> <option value="today">Today</option> <option value="7d" selected>Last 7 days</option> <option value="30d">Last 30 days</option> <option value="all">All time</option> </select> </div> <div class="grid-cards" id="kpis"></div> <div class="card" style="margin-top:1.2rem;"> <h3>Tool leaderboard</h3> <div id="leaderboard" class="chart-tall"></div> </div> <div class="grid-2" style="margin-top:1.2rem;" id="splits"> <div class="card"> <h3>MCP servers</h3> <div id="mcp" class="chart"></div> </div> <div class="card"> <h3>Sub-agent invocations</h3> <div id="subagents" class="chart"></div> </div> </div> <div class="card" style="margin-top:1.2rem;"> <h3>Per-tool details</h3> <table id="tools-table"> <thead> <tr> <th>Tool</th> <th class="num">Calls</th> <th class="num">Errors</th> <th class="num">Error rate</th> </tr> </thead> <tbody></tbody> </table> </div> <script type="module" src="/_astro/tools.astro_astro_type_script_index_0_lang.
|
|
1
|
+
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Argus — Tools</title><link rel="stylesheet" href="/styles/global.css"></head> <body> <header class="app-header"> <a href="/" class="brand">ARGUS<span class="dot">●</span></a> <nav class="app-nav"> <a href="/" class>Overview</a> <a href="/sessions" class>Sessions</a> <a href="/tools" class>Tools</a> <a href="/prompts" class>Prompts</a> <a href="/trends" class>Trends</a> <a href="/models" class>Models</a> <a href="/settings" class>Settings</a> </nav> </header> <main> <h2>Tool usage</h2> <div class="card" style="margin-bottom:1rem;display:flex;gap:0.6rem;align-items:center;"> <label style="color:var(--text-2);font-size:0.8rem;">Window</label> <select id="w"> <option value="today">Today</option> <option value="7d" selected>Last 7 days</option> <option value="30d">Last 30 days</option> <option value="all">All time</option> </select> </div> <div class="grid-cards" id="kpis"></div> <div class="card" style="margin-top:1.2rem;"> <h3>Tool leaderboard</h3> <div id="leaderboard" class="chart-tall"></div> </div> <div class="grid-2" style="margin-top:1.2rem;" id="splits"> <div class="card"> <h3>MCP servers</h3> <div id="mcp" class="chart"></div> </div> <div class="card"> <h3>Sub-agent invocations</h3> <div id="subagents" class="chart"></div> </div> </div> <div class="card" style="margin-top:1.2rem;"> <h3>Per-tool details</h3> <table id="tools-table"> <thead> <tr> <th>Tool</th> <th class="num">Calls</th> <th class="num">Errors</th> <th class="num">Error rate</th> </tr> </thead> <tbody></tbody> </table> </div> <script type="module" src="/_astro/tools.astro_astro_type_script_index_0_lang.DHTUv2RL.js"></script> </main> <footer class="app-footer" id="footer"> <span class="status-pill" id="ingest-status">Loading…</span> <span style="margin: 0 0.8rem;">·</span> <span id="pricing-info">Pricing —</span> <span id="daemon-status" style="margin-left: 0.8rem; display: none;" title="Ingestion is handled by the argusd background daemon">· Powered by argusd ●</span> </footer> <script type="module">async function c(){try{const[n,e]=await Promise.all([fetch("/api/pricing").then(s=>s.json()),fetch("/api/ingest/status").then(s=>s.json())]),o=document.getElementById("pricing-info");o&&(o.innerHTML=`Costs estimated from pricing table v<code>${n.version}</code> — tokens are exact; <code>argus pricing refresh</code> to update`);const t=document.getElementById("ingest-status");if(t)if(e.foregroundComplete&&e.pending===0){const s=typeof e.sessionCount=="number"?e.sessionCount:e.processed;t.textContent=`Tracking ${s} sessions`,t.classList.remove("busy")}else e.foregroundComplete?(t.textContent=`Backfilling history... ${e.processed}/${e.total}`,t.classList.add("busy")):(t.textContent=`Ingesting recent... ${e.processed}/${e.total}`,t.classList.add("busy"));const i=document.getElementById("daemon-status");i&&(i.style.display=e.daemon===!0?"inline":"none")}catch{}}c();setInterval(c,4e3);</script> </body> </html>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# AGENTS.md — Python backend package (`argus`)
|
|
2
|
+
|
|
3
|
+
Parent: repo-root `AGENTS.md`. Read it first for the workflow and global rules.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The `argus` package: ingest Claude Code transcripts, store them in SQLite, serve
|
|
8
|
+
the dashboard, and provide the `argus` CLI. Runs directly from source
|
|
9
|
+
(`uv run argus start`) — no build step. Entry point: `argus = "argus.cli:app"`.
|
|
10
|
+
|
|
11
|
+
## Ownership
|
|
12
|
+
|
|
13
|
+
This doc owns backend-wide rules and the subsystems that have no child doc:
|
|
14
|
+
- `core/` — runtime wiring (`RuntimeOptions`, building the watcher + server).
|
|
15
|
+
- `daemon/` — `argusd` background service: pidfile, process lifecycle, logging.
|
|
16
|
+
- `detectors/` — alert detectors (registry + individual rules like tool-error-rate spike).
|
|
17
|
+
- `pricing/` — pricing table load / refresh / compute; bundled JSON under repo `pricing/`.
|
|
18
|
+
- `scaffold/` — `argus claude` scaffolding (templates, snapshot, storage).
|
|
19
|
+
- `schema/` — shared pydantic types (`Session`, `Turn`, …). Changing a stored
|
|
20
|
+
field ripples into `store/` (columns) and `server/` (serialization).
|
|
21
|
+
|
|
22
|
+
Delegated subtrees (see their own AGENTS.md): `store/`, `collector/`, `adapters/`, `server/`.
|
|
23
|
+
|
|
24
|
+
## Local Contracts
|
|
25
|
+
|
|
26
|
+
- **Version lives in two files** that must stay in lock-step: `__init__.py`
|
|
27
|
+
(`__version__`) and repo-root `pyproject.toml`. Bump both on release (root doc).
|
|
28
|
+
- **CLI command groups** are Typer apps wired in `cli.py`. When renaming a
|
|
29
|
+
user-facing command, keep the old name as a `hidden=True` alias and nudge to the
|
|
30
|
+
new one (precedent: `search` → `indexing`). Update help strings, the daemon
|
|
31
|
+
read-only API hint, and dashboard copy together.
|
|
32
|
+
- **Privacy stays intact:** no network calls or telemetry beyond opt-in
|
|
33
|
+
`argus pricing refresh`. Don't add outbound calls.
|
|
34
|
+
|
|
35
|
+
## Work Guidance
|
|
36
|
+
|
|
37
|
+
- Follow existing module patterns; keep files focused and small.
|
|
38
|
+
- Python ≥ 3.11. Dependencies are deliberately minimal (fastapi, uvicorn,
|
|
39
|
+
pydantic, watchdog, typer, httpx) — don't add deps without cause.
|
|
40
|
+
|
|
41
|
+
## Verification
|
|
42
|
+
|
|
43
|
+
`uv run pytest` (full suite). Subsystem tests live under `tests/<subsystem>/`.
|
|
44
|
+
|
|
45
|
+
## Child DOX Index
|
|
46
|
+
|
|
47
|
+
- `store/AGENTS.md` — SQLite, migrations, repository; data-safety + path normalization.
|
|
48
|
+
- `collector/AGENTS.md` — ingest pipeline and missing-data backfill.
|
|
49
|
+
- `adapters/AGENTS.md` — Claude Code adapter and transcript-segment extraction.
|
|
50
|
+
- `server/AGENTS.md` — FastAPI routes, static serving, clean shutdown.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# AGENTS.md — adapters (Claude Code transcript adapter)
|
|
2
|
+
|
|
3
|
+
Parent: `python/argus/AGENTS.md`. Parses agent transcript files into typed results.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
`claude_code/` reads Claude Code's `.jsonl` transcripts: `adapter.py` discovers and
|
|
8
|
+
ingests files, `schemas.py` validates each line (`AssistantLine`, `UserLine`, …),
|
|
9
|
+
`extract_transcript.py` derives searchable segments.
|
|
10
|
+
|
|
11
|
+
## Local Contracts
|
|
12
|
+
|
|
13
|
+
- **Discovery excludes sub-agent files.** `discover_session_files()` returns
|
|
14
|
+
top-level session files only; sub-agent transcripts are reached via
|
|
15
|
+
`sub_session_files_for(parent_file)`, which walks the parent's `subagents/`
|
|
16
|
+
directory (including the nested `subagents/workflows/<wf>/agent-*.jsonl` layout).
|
|
17
|
+
- **Sub-agent session id** = `<parent_session_id>/<sub_file_stem>` (e.g.
|
|
18
|
+
`claude_code:<parent>/agent-<hex>`). The `/` is what marks a row as a sub-agent
|
|
19
|
+
everywhere downstream.
|
|
20
|
+
- **`extract_transcript_segments`** emits `RawSegment`s with roles `assistant`,
|
|
21
|
+
`thinking`, `user`, and `tool_result`; each is byte-capped (`_cap_text`). The
|
|
22
|
+
first `user` segment of a sub-agent is its "Task given" in the dashboard.
|
|
23
|
+
- **Known data limitation — do not design against it:** `subagent_type` is empty
|
|
24
|
+
for every row, sub-agent ids are exactly one level deep, and there is no stored
|
|
25
|
+
workflow grouping or spawn-turn link. Don't build features that assume a nested
|
|
26
|
+
sub-agent tree, type pills, or a spawn→child edge; the data isn't there.
|
|
27
|
+
- **`ingest_file(path, offset)` is pure-ish:** it parses from a byte offset and
|
|
28
|
+
returns `(result, new_offset)`. It does not write to the DB — `collector/` owns
|
|
29
|
+
persistence and offset bookkeeping.
|
|
30
|
+
- **Path containment is enforced at `ClaudeCodeAdapter.ingest_file`, and that is
|
|
31
|
+
deliberate.** It is the one choke point every read reaches — discovery, the
|
|
32
|
+
watcher's fs events, and the pipeline's sub-agent walk — so a path that doesn't
|
|
33
|
+
`resolve()` under the adapter's root gets an empty result and a logged warning
|
|
34
|
+
instead of being read. Don't move this check out to the callers: it lived only
|
|
35
|
+
in `discover_session_files()` once, and `sub_agent_files_for()` plus the watcher
|
|
36
|
+
silently bypassed it into an arbitrary-file-read (bytes reached `parse_errors`,
|
|
37
|
+
which `GET /api/parse-errors` serves out). `sub_agent_files_for()` now *also*
|
|
38
|
+
takes `claude_root` and filters — required, not optional, so a new caller must
|
|
39
|
+
confront it. Covers symlinks and NTFS junctions alike (both resolve out).
|
|
40
|
+
|
|
41
|
+
## Verification
|
|
42
|
+
|
|
43
|
+
`uv run pytest tests/adapters`. Real-`~/.claude/` tests are gated by
|
|
44
|
+
`ARGUS_REAL_CLAUDE_ROOT` and skipped otherwise.
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
"""The Claude Code adapter — façade that wires the package together."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import logging
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
from ..base import AdapterIngestResult
|
|
8
9
|
from ..registry import register
|
|
9
|
-
from .discover import discover_session_files, sub_agent_files_for
|
|
10
|
+
from .discover import _safe_realpath_under, discover_session_files, sub_agent_files_for
|
|
10
11
|
from .history_jsonl import ingest_history_file
|
|
11
|
-
from .ingest_file import ingest_claude_code_file
|
|
12
|
+
from .ingest_file import _empty_result, ingest_claude_code_file
|
|
12
13
|
from .model import canonicalize_claude_model
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from ...store.repository import Repository
|
|
16
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
def _default_root() -> Path:
|
|
19
22
|
return Path.home() / ".claude"
|
|
@@ -38,6 +41,14 @@ class ClaudeCodeAdapter:
|
|
|
38
41
|
def ingest_file(
|
|
39
42
|
self, path: Path, from_offset: int = 0
|
|
40
43
|
) -> tuple[AdapterIngestResult, int]:
|
|
44
|
+
# Containment lives here, at the one choke point every read passes
|
|
45
|
+
# through -- discovery, the watcher's fs events, and the sub-agent
|
|
46
|
+
# walk in the pipeline all land on this method. Checking here rather
|
|
47
|
+
# than at each caller is what stops a future call site from quietly
|
|
48
|
+
# reopening the arbitrary-file-read hole (see discover.py's rules).
|
|
49
|
+
if _safe_realpath_under(path, self._root) is None:
|
|
50
|
+
logger.warning("refusing to ingest a path outside the claude root: %s", path)
|
|
51
|
+
return _empty_result(path), from_offset
|
|
41
52
|
return ingest_claude_code_file(path, from_offset)
|
|
42
53
|
|
|
43
54
|
# ─── Extension-point overrides ─────────────────────────────────────
|
|
@@ -52,7 +63,7 @@ class ClaudeCodeAdapter:
|
|
|
52
63
|
ingest_history_file(path, repo)
|
|
53
64
|
|
|
54
65
|
def sub_session_files_for(self, session_file: Path) -> list[Path]:
|
|
55
|
-
return sub_agent_files_for(session_file)
|
|
66
|
+
return sub_agent_files_for(session_file, self._root)
|
|
56
67
|
|
|
57
68
|
def should_skip(self, path: Path) -> bool:
|
|
58
69
|
# The subagents/ tree is walked as part of the parent's ingest,
|
|
@@ -66,7 +66,7 @@ def discover_session_files(claude_root: Path) -> list[Path]:
|
|
|
66
66
|
return out
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def sub_agent_files_for(session_file: Path) -> list[Path]:
|
|
69
|
+
def sub_agent_files_for(session_file: Path, claude_root: Path) -> list[Path]:
|
|
70
70
|
"""Return the sub-agent transcript JSONLs for a session.
|
|
71
71
|
|
|
72
72
|
Two on-disk layouts coexist under ``<session_dir>/<sid>/subagents/``:
|
|
@@ -88,12 +88,25 @@ def sub_agent_files_for(session_file: Path) -> list[Path]:
|
|
|
88
88
|
sub = session_file.parent / sid / "subagents"
|
|
89
89
|
if not sub.exists():
|
|
90
90
|
return []
|
|
91
|
+
try:
|
|
92
|
+
canonical_root = claude_root.resolve(strict=True)
|
|
93
|
+
except (OSError, RuntimeError):
|
|
94
|
+
return []
|
|
91
95
|
out: set[Path] = set()
|
|
92
96
|
# Flat transcripts directly under subagents/ (dirs like workflows/ have
|
|
93
97
|
# no .jsonl suffix, so they're skipped here).
|
|
94
98
|
out.update(f for f in sub.iterdir() if f.is_file() and f.suffix == ".jsonl")
|
|
95
99
|
# Nested Workflow sub-agent transcripts, any depth below subagents/.
|
|
96
100
|
out.update(f for f in sub.rglob("agent-*.jsonl") if f.is_file())
|
|
101
|
+
# Same containment rule as discover_session_files: a symlink planted in
|
|
102
|
+
# subagents/ must not turn into an arbitrary file read. (rglob does not
|
|
103
|
+
# follow directory symlinks, but a symlinked *file* still resolves out.)
|
|
104
|
+
dropped = {f for f in out if _safe_realpath_under(f, canonical_root) is None}
|
|
105
|
+
for f in sorted(dropped):
|
|
106
|
+
logger.warning(
|
|
107
|
+
"skipping sub-agent transcript that escapes the claude root: %s", f
|
|
108
|
+
)
|
|
109
|
+
out -= dropped
|
|
97
110
|
|
|
98
111
|
if not out and any(c.is_dir() for c in sub.iterdir()):
|
|
99
112
|
# subagents/ has subdirectories but nothing matched — the layout
|
|
@@ -15,7 +15,11 @@ import argus.detectors # noqa: F401 — triggers @register side-effects
|
|
|
15
15
|
from .pricing.load import load_pricing_table
|
|
16
16
|
from .pricing.refresh import diff_pricing, fetch_litellm_table
|
|
17
17
|
from .scaffold.scaffolder import scaffold_project
|
|
18
|
-
from .scaffold.snapshot import
|
|
18
|
+
from .scaffold.snapshot import (
|
|
19
|
+
secret_files_in,
|
|
20
|
+
snapshot_candidates,
|
|
21
|
+
snapshot_template,
|
|
22
|
+
)
|
|
19
23
|
from .scaffold.storage import RESERVED_TEMPLATE_NAMES, list_templates, resolve_template
|
|
20
24
|
from .server.app import ServerOpts, build_app, serve_blocking
|
|
21
25
|
from .store.db import open_db
|
|
@@ -288,6 +292,14 @@ def claude_init(
|
|
|
288
292
|
"""Scaffold a .claude/ setup (and CLAUDE.md) into a project."""
|
|
289
293
|
try:
|
|
290
294
|
template_dir = resolve_template(template, data_dir)
|
|
295
|
+
except ValueError:
|
|
296
|
+
# Path-like name (e.g. ../../x) — rejected before it touches the disk.
|
|
297
|
+
typer.echo(
|
|
298
|
+
f"Invalid template name '{template}'. "
|
|
299
|
+
"Use a plain name: letters, digits, dot, dash, underscore.",
|
|
300
|
+
err=True,
|
|
301
|
+
)
|
|
302
|
+
raise typer.Exit(code=1)
|
|
291
303
|
except KeyError:
|
|
292
304
|
avail = ", ".join(list_templates(data_dir)) or "(none)"
|
|
293
305
|
typer.echo(f"Unknown template '{template}'. Available: {avail}", err=True)
|
|
@@ -358,6 +370,17 @@ def template_create(
|
|
|
358
370
|
typer.echo(str(e), err=True)
|
|
359
371
|
raise typer.Exit(code=1)
|
|
360
372
|
typer.echo(f"Created template '{name}' at {target}")
|
|
373
|
+
# Say what was withheld. A credential filter that drops files silently is
|
|
374
|
+
# its own bug — the user needs to know the template is deliberately
|
|
375
|
+
# incomplete, and why.
|
|
376
|
+
withheld = secret_files_in(project / ".claude")
|
|
377
|
+
if withheld:
|
|
378
|
+
typer.echo(
|
|
379
|
+
"\nLeft out of the template (looks like credentials): "
|
|
380
|
+
+ ", ".join(withheld)
|
|
381
|
+
+ "\nTemplates get copied into every project you scaffold, so "
|
|
382
|
+
"secrets are never snapshotted."
|
|
383
|
+
)
|
|
361
384
|
|
|
362
385
|
|
|
363
386
|
@app.command()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# AGENTS.md — collector (ingest pipeline + backfill)
|
|
2
|
+
|
|
3
|
+
Parent: `python/argus/AGENTS.md`. Turns transcript files into stored rows.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
`pipeline.py` ingests one file tick; `first_run.py` orchestrates the first-pass
|
|
8
|
+
ingest (foreground recent files, background older files) and backfills derived data
|
|
9
|
+
after a schema/feature upgrade.
|
|
10
|
+
|
|
11
|
+
## Local Contracts
|
|
12
|
+
|
|
13
|
+
- **Offset-driven reads.** `ingest_file` reads only bytes after the stored file
|
|
14
|
+
offset, upserts turns/tool_calls/segments, then recomputes the session. A
|
|
15
|
+
fully-read file sits at EOF and is **not** re-read on the next tick. To force a
|
|
16
|
+
re-extract you MUST reset its offset to 0 (`repo.set_file_offset(path, 0)`) —
|
|
17
|
+
re-calling `ingest_file` alone does nothing for an EOF file.
|
|
18
|
+
- **Sub-agents are walked via the parent.** A parent ingest discovers
|
|
19
|
+
`adapter.sub_session_files_for(parent)` and ingests any that **grew past their
|
|
20
|
+
offset**. Sub-agent session ids contain `/` (`<parent>/agent-<hex>`).
|
|
21
|
+
- **Segments are gated on indexing.** Parent and sub-agent segments are written
|
|
22
|
+
only when `repo.is_search_indexing_enabled()` (see `store/AGENTS.md`). So
|
|
23
|
+
enabling indexing *after* ingest requires re-reading the relevant files.
|
|
24
|
+
- **Backfill (`_backfill_missing_derived_data`) routes through parents.** Ids with
|
|
25
|
+
`/` are skipped by the processing loop ("walked via parents"). Anything needing a
|
|
26
|
+
sub-agent file re-read must add the **parent** id to both the work set and
|
|
27
|
+
`deep_reset` (deep_reset zeroes the sub-agent file offsets). Missing-segment
|
|
28
|
+
sessions therefore map to their parent + deep_reset — otherwise sub-agent
|
|
29
|
+
segments never backfill and the dashboard's "Task given" stays empty.
|
|
30
|
+
- **Bounded per run.** Backfill candidates are capped (200) per `argus start`;
|
|
31
|
+
large installs may need several restarts to converge. If you add a new bounded
|
|
32
|
+
sweep, surface what was deferred rather than silently truncating.
|
|
33
|
+
|
|
34
|
+
## Verification
|
|
35
|
+
|
|
36
|
+
`uv run pytest tests/collector` — covers incremental re-ingest, sub-agent
|
|
37
|
+
backfill, segment gating, and the "enable indexing late" path.
|
|
@@ -20,6 +20,12 @@ from . import pidfile
|
|
|
20
20
|
def daemon_argv(data_dir: Path) -> list[str]:
|
|
21
21
|
return [
|
|
22
22
|
sys.executable,
|
|
23
|
+
# -P (3.11+) keeps the *caller's* cwd off the child's sys.path. Without
|
|
24
|
+
# it, `-m` prepends cwd, so running `argus daemon start` from inside a
|
|
25
|
+
# checkout that ships its own `argus/` directory would run that code
|
|
26
|
+
# instead of the installed package -- arbitrary execution in a detached
|
|
27
|
+
# process that outlives the shell.
|
|
28
|
+
"-P",
|
|
23
29
|
"-m",
|
|
24
30
|
"argus.cli",
|
|
25
31
|
"daemon",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Copy a template directory into a project — the `argus claude init` core."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import os
|
|
4
5
|
import shutil
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from pathlib import Path
|
|
@@ -9,6 +10,25 @@ from pathlib import Path
|
|
|
9
10
|
_CLAUDE_MD = "CLAUDE.md"
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def _resolves_inside(target: Path, root: Path) -> bool:
|
|
14
|
+
"""True if ``target`` canonicalises to ``root`` or below it.
|
|
15
|
+
|
|
16
|
+
``resolve()`` follows every symlink and NTFS junction on the path —
|
|
17
|
+
including the final component, and including a *dangling* link — which is
|
|
18
|
+
exactly what we need. ``shutil.copyfile`` writes *through* a symlink, and
|
|
19
|
+
the caller's ``target.exists()`` guard follows one too (so a dangling link
|
|
20
|
+
reports "doesn't exist" and then gets created on write). Comparing
|
|
21
|
+
canonical paths is the only check that catches all of those.
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
resolved = target.resolve()
|
|
25
|
+
except (OSError, RuntimeError):
|
|
26
|
+
return False
|
|
27
|
+
a = os.path.normcase(str(resolved))
|
|
28
|
+
b = os.path.normcase(str(root))
|
|
29
|
+
return a == b or a.startswith(b + os.sep)
|
|
30
|
+
|
|
31
|
+
|
|
12
32
|
@dataclass
|
|
13
33
|
class ScaffoldResult:
|
|
14
34
|
created: list[Path] = field(default_factory=list)
|
|
@@ -26,11 +46,20 @@ def scaffold_project(
|
|
|
26
46
|
- ``CLAUDE.md`` is NEVER overwritten, even with ``force``.
|
|
27
47
|
"""
|
|
28
48
|
result = ScaffoldResult()
|
|
49
|
+
dest_root = dest.resolve()
|
|
29
50
|
for src in sorted(p for p in template_dir.rglob("*") if p.is_file()):
|
|
30
51
|
rel = src.relative_to(template_dir)
|
|
31
52
|
target = dest / rel
|
|
32
53
|
is_claude_md = rel.as_posix() == _CLAUDE_MD
|
|
33
54
|
|
|
55
|
+
# Never write outside the destination. A hostile repo can ship
|
|
56
|
+
# `.claude/` as a symlink/junction, or `.claude/settings.json` as a
|
|
57
|
+
# link to ~/.ssh/authorized_keys; cloning it and running
|
|
58
|
+
# `argus claude init` would otherwise write through that link.
|
|
59
|
+
if not _resolves_inside(target, dest_root):
|
|
60
|
+
result.skipped.append((target, "would write outside the project"))
|
|
61
|
+
continue
|
|
62
|
+
|
|
34
63
|
if target.exists():
|
|
35
64
|
if is_claude_md:
|
|
36
65
|
result.skipped.append((target, "CLAUDE.md exists, left untouched"))
|