argus-code 0.2.0__tar.gz → 0.3.1__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.
Files changed (185) hide show
  1. {argus_code-0.2.0 → argus_code-0.3.1}/PKG-INFO +68 -3
  2. {argus_code-0.2.0 → argus_code-0.3.1}/README.md +67 -2
  3. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/layouts/Default.astro +3 -0
  4. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/pages/index.astro +12 -14
  5. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/pages/session.astro +1 -1
  6. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/pages/settings.astro +11 -0
  7. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/scripts/charts.ts +14 -13
  8. argus_code-0.3.1/dashboard-dist/_astro/charts.CAJCDcsn.js +1 -0
  9. argus_code-0.3.1/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.Dtzf0Pdc.js +24 -0
  10. argus_code-0.2.0/dashboard-dist/_astro/session.astro_astro_type_script_index_0_lang.Dj_bfrIa.js → argus_code-0.3.1/dashboard-dist/_astro/session.astro_astro_type_script_index_0_lang.C2_GW8Bb.js +86 -86
  11. argus_code-0.3.1/dashboard-dist/_astro/settings.astro_astro_type_script_index_0_lang.C--f3VLy.js +24 -0
  12. argus_code-0.2.0/dashboard-dist/_astro/trends.astro_astro_type_script_index_0_lang.BLLeGRNa.js → argus_code-0.3.1/dashboard-dist/_astro/trends.astro_astro_type_script_index_0_lang.BZ0GmC-o.js +1 -1
  13. argus_code-0.3.1/dashboard-dist/index.html +2 -0
  14. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/models/index.html +1 -1
  15. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/prompts/index.html +1 -1
  16. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/session/index.html +2 -2
  17. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/sessions/index.html +1 -1
  18. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/settings/index.html +1 -1
  19. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/tools/index.html +1 -1
  20. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/trends/index.html +1 -1
  21. {argus_code-0.2.0 → argus_code-0.3.1}/pyproject.toml +1 -1
  22. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/__init__.py +1 -1
  23. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/cli.py +153 -42
  24. argus_code-0.3.1/python/argus/core/runtime.py +119 -0
  25. argus_code-0.3.1/python/argus/daemon/logging.py +103 -0
  26. argus_code-0.3.1/python/argus/daemon/pidfile.py +79 -0
  27. argus_code-0.3.1/python/argus/daemon/process.py +107 -0
  28. argus_code-0.3.1/python/argus/daemon/service.py +70 -0
  29. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/server/api.py +31 -2
  30. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/server/app.py +2 -0
  31. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/store/db.py +25 -2
  32. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/store/repository.py +82 -25
  33. argus_code-0.3.1/tests/core/test_runtime.py +178 -0
  34. argus_code-0.3.1/tests/daemon/test_logs.py +70 -0
  35. argus_code-0.3.1/tests/daemon/test_pidfile.py +39 -0
  36. argus_code-0.3.1/tests/daemon/test_process.py +85 -0
  37. argus_code-0.3.1/tests/daemon/test_service.py +59 -0
  38. argus_code-0.3.1/tests/daemon/test_yield.py +26 -0
  39. argus_code-0.3.1/tests/scaffold/__init__.py +0 -0
  40. argus_code-0.3.1/tests/schema/__init__.py +0 -0
  41. argus_code-0.3.1/tests/server/__init__.py +0 -0
  42. {argus_code-0.2.0 → argus_code-0.3.1}/tests/server/test_api.py +58 -1
  43. {argus_code-0.2.0 → argus_code-0.3.1}/tests/server/test_api_search.py +41 -0
  44. argus_code-0.3.1/tests/store/__init__.py +0 -0
  45. {argus_code-0.2.0 → argus_code-0.3.1}/tests/store/test_db.py +27 -0
  46. {argus_code-0.2.0 → argus_code-0.3.1}/tests/store/test_repository.py +69 -0
  47. {argus_code-0.2.0 → argus_code-0.3.1}/uv.lock +983 -983
  48. argus_code-0.2.0/dashboard-dist/_astro/charts.BIevw6Es.js +0 -1
  49. argus_code-0.2.0/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.CgwSARdD.js +0 -24
  50. argus_code-0.2.0/dashboard-dist/_astro/settings.astro_astro_type_script_index_0_lang.d_a-uvdi.js +0 -24
  51. argus_code-0.2.0/dashboard-dist/index.html +0 -2
  52. {argus_code-0.2.0 → argus_code-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  53. {argus_code-0.2.0 → argus_code-0.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  54. {argus_code-0.2.0 → argus_code-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  55. {argus_code-0.2.0 → argus_code-0.3.1}/.github/pull_request_template.md +0 -0
  56. {argus_code-0.2.0 → argus_code-0.3.1}/.gitignore +0 -0
  57. {argus_code-0.2.0 → argus_code-0.3.1}/.npmrc +0 -0
  58. {argus_code-0.2.0 → argus_code-0.3.1}/ARCHITECTURE.md +0 -0
  59. {argus_code-0.2.0 → argus_code-0.3.1}/CONTRIBUTING.md +0 -0
  60. {argus_code-0.2.0 → argus_code-0.3.1}/LICENSE +0 -0
  61. {argus_code-0.2.0 → argus_code-0.3.1}/NOTICE +0 -0
  62. {argus_code-0.2.0 → argus_code-0.3.1}/PRD.md +0 -0
  63. {argus_code-0.2.0 → argus_code-0.3.1}/SECURITY.md +0 -0
  64. {argus_code-0.2.0 → argus_code-0.3.1}/TESTING.md +0 -0
  65. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/.astro/content-assets.mjs +0 -0
  66. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/.astro/content-modules.mjs +0 -0
  67. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/.astro/content.d.ts +0 -0
  68. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/.astro/types.d.ts +0 -0
  69. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/astro.config.mjs +0 -0
  70. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/package-lock.json +0 -0
  71. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/package.json +0 -0
  72. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/public/styles/global.css +0 -0
  73. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/pages/models.astro +0 -0
  74. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/pages/prompts.astro +0 -0
  75. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/pages/sessions/index.astro +0 -0
  76. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/pages/tools.astro +0 -0
  77. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/pages/trends.astro +0 -0
  78. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/scripts/alerts.ts +0 -0
  79. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/scripts/api.ts +0 -0
  80. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/scripts/format.ts +0 -0
  81. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/src/styles/global.css +0 -0
  82. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard/tsconfig.json +0 -0
  83. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/_astro/format.DxC1NGYT.js +0 -0
  84. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.W18SJsr7.js +0 -0
  85. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/_astro/installCanvasRenderer.D_tC6TXz.js +0 -0
  86. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/_astro/models.astro_astro_type_script_index_0_lang.BHTHXYHC.js +0 -0
  87. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/_astro/prompts.astro_astro_type_script_index_0_lang.DfNgiDv9.js +0 -0
  88. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/_astro/tools.astro_astro_type_script_index_0_lang.Dzzau3Yt.js +0 -0
  89. {argus_code-0.2.0 → argus_code-0.3.1}/dashboard-dist/styles/global.css +0 -0
  90. {argus_code-0.2.0 → argus_code-0.3.1}/pricing/2026-05-02.json +0 -0
  91. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/__init__.py +0 -0
  92. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/base.py +0 -0
  93. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/__init__.py +0 -0
  94. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/adapter.py +0 -0
  95. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/discover.py +0 -0
  96. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/extract_tool_calls.py +0 -0
  97. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/extract_transcript.py +0 -0
  98. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/extract_turns.py +0 -0
  99. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/history_jsonl.py +0 -0
  100. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/ingest_file.py +0 -0
  101. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/model.py +0 -0
  102. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/claude_code/schemas.py +0 -0
  103. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/adapters/registry.py +0 -0
  104. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/collector/__init__.py +0 -0
  105. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/collector/aggregate.py +0 -0
  106. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/collector/first_run.py +0 -0
  107. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/collector/pipeline.py +0 -0
  108. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/collector/rollup_subagents.py +0 -0
  109. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/collector/scheduler.py +0 -0
  110. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/collector/search_backfill.py +0 -0
  111. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/collector/watcher.py +0 -0
  112. {argus_code-0.2.0/python/argus/pricing → argus_code-0.3.1/python/argus/core}/__init__.py +0 -0
  113. {argus_code-0.2.0/python/argus/scaffold → argus_code-0.3.1/python/argus/daemon}/__init__.py +0 -0
  114. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/detectors/__init__.py +0 -0
  115. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/detectors/base.py +0 -0
  116. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/detectors/registry.py +0 -0
  117. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/detectors/tool_error_rate_spike.py +0 -0
  118. {argus_code-0.2.0/python/argus/schema → argus_code-0.3.1/python/argus/pricing}/__init__.py +0 -0
  119. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/pricing/compute.py +0 -0
  120. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/pricing/load.py +0 -0
  121. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/pricing/refresh.py +0 -0
  122. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/pricing/types.py +0 -0
  123. {argus_code-0.2.0/python/argus/server → argus_code-0.3.1/python/argus/scaffold}/__init__.py +0 -0
  124. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/scaffold/scaffolder.py +0 -0
  125. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/scaffold/snapshot.py +0 -0
  126. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/scaffold/storage.py +0 -0
  127. {argus_code-0.2.0/python/argus/store → argus_code-0.3.1/python/argus/schema}/__init__.py +0 -0
  128. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/schema/types.py +0 -0
  129. {argus_code-0.2.0/python/argus/store/migrations → argus_code-0.3.1/python/argus/server}/__init__.py +0 -0
  130. {argus_code-0.2.0/tests → argus_code-0.3.1/python/argus/store}/__init__.py +0 -0
  131. {argus_code-0.2.0/tests/adapters → argus_code-0.3.1/python/argus/store/migrations}/__init__.py +0 -0
  132. {argus_code-0.2.0 → argus_code-0.3.1}/python/argus/store/migrations/inline.py +0 -0
  133. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/agents/code-reviewer.md +0 -0
  134. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/agents/security-auditor.md +0 -0
  135. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/commands/commit.md +0 -0
  136. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/commands/deploy.md +0 -0
  137. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/commands/fix-issue.md +0 -0
  138. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/commands/pr.md +0 -0
  139. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/commands/review.md +0 -0
  140. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/rules/api-conventions.md +0 -0
  141. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/rules/code-style.md +0 -0
  142. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/rules/testing.md +0 -0
  143. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/settings.json +0 -0
  144. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/.claude/skills/example/SKILL.md +0 -0
  145. {argus_code-0.2.0 → argus_code-0.3.1}/templates/default/CLAUDE.md +0 -0
  146. {argus_code-0.2.0/tests/adapters/claude_code → argus_code-0.3.1/tests}/__init__.py +0 -0
  147. {argus_code-0.2.0/tests/collector → argus_code-0.3.1/tests/adapters}/__init__.py +0 -0
  148. {argus_code-0.2.0/tests/detectors → argus_code-0.3.1/tests/adapters/claude_code}/__init__.py +0 -0
  149. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_adapter.py +0 -0
  150. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_discover.py +0 -0
  151. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_extract_tool_calls.py +0 -0
  152. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_extract_transcript.py +0 -0
  153. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_extract_turns.py +0 -0
  154. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_history_jsonl.py +0 -0
  155. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_ingest_file.py +0 -0
  156. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_integration.py +0 -0
  157. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_model.py +0 -0
  158. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/claude_code/test_schemas.py +0 -0
  159. {argus_code-0.2.0 → argus_code-0.3.1}/tests/adapters/test_registry.py +0 -0
  160. {argus_code-0.2.0/tests/pricing → argus_code-0.3.1/tests/collector}/__init__.py +0 -0
  161. {argus_code-0.2.0 → argus_code-0.3.1}/tests/collector/test_aggregate.py +0 -0
  162. {argus_code-0.2.0 → argus_code-0.3.1}/tests/collector/test_first_run.py +0 -0
  163. {argus_code-0.2.0 → argus_code-0.3.1}/tests/collector/test_pipeline.py +0 -0
  164. {argus_code-0.2.0 → argus_code-0.3.1}/tests/collector/test_rollup_subagents.py +0 -0
  165. {argus_code-0.2.0 → argus_code-0.3.1}/tests/collector/test_scheduler.py +0 -0
  166. {argus_code-0.2.0 → argus_code-0.3.1}/tests/collector/test_watcher.py +0 -0
  167. {argus_code-0.2.0 → argus_code-0.3.1}/tests/conftest.py +0 -0
  168. {argus_code-0.2.0/tests/scaffold → argus_code-0.3.1/tests/core}/__init__.py +0 -0
  169. {argus_code-0.2.0/tests/schema → argus_code-0.3.1/tests/daemon}/__init__.py +0 -0
  170. {argus_code-0.2.0/tests/server → argus_code-0.3.1/tests/detectors}/__init__.py +0 -0
  171. {argus_code-0.2.0 → argus_code-0.3.1}/tests/detectors/test_registry.py +0 -0
  172. {argus_code-0.2.0 → argus_code-0.3.1}/tests/detectors/test_tool_error_rate_spike.py +0 -0
  173. {argus_code-0.2.0/tests/store → argus_code-0.3.1/tests/pricing}/__init__.py +0 -0
  174. {argus_code-0.2.0 → argus_code-0.3.1}/tests/pricing/test_compute.py +0 -0
  175. {argus_code-0.2.0 → argus_code-0.3.1}/tests/pricing/test_load.py +0 -0
  176. {argus_code-0.2.0 → argus_code-0.3.1}/tests/pricing/test_refresh.py +0 -0
  177. {argus_code-0.2.0 → argus_code-0.3.1}/tests/scaffold/test_bundled_template.py +0 -0
  178. {argus_code-0.2.0 → argus_code-0.3.1}/tests/scaffold/test_cli_claude.py +0 -0
  179. {argus_code-0.2.0 → argus_code-0.3.1}/tests/scaffold/test_scaffolder.py +0 -0
  180. {argus_code-0.2.0 → argus_code-0.3.1}/tests/scaffold/test_snapshot.py +0 -0
  181. {argus_code-0.2.0 → argus_code-0.3.1}/tests/scaffold/test_storage.py +0 -0
  182. {argus_code-0.2.0 → argus_code-0.3.1}/tests/schema/test_types.py +0 -0
  183. {argus_code-0.2.0 → argus_code-0.3.1}/tests/server/test_server.py +0 -0
  184. {argus_code-0.2.0 → argus_code-0.3.1}/tests/server/test_week_of.py +0 -0
  185. {argus_code-0.2.0 → argus_code-0.3.1}/tests/test_e2e.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argus-code
3
- Version: 0.2.0
3
+ Version: 0.3.1
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
@@ -106,11 +106,68 @@ argus # top-level command group
106
106
  │ ├─ list # list templates (bundled + user)
107
107
  │ └─ create <name> [--path <dir>] [--all]
108
108
  │ # save a project's .claude/ as a template
109
+ ├─ daemon # argusd — background ingestion + detectors
110
+ │ ├─ start # start argusd detached, write PID file
111
+ │ ├─ stop # stop argusd gracefully
112
+ │ ├─ restart # stop then start
113
+ │ ├─ status # running? PID + uptime
114
+ │ └─ logs [-n N] [-f] # tail ~/.argus/argusd.log
109
115
  └─ wipe # delete ~/.argus/ entirely
110
116
  ```
111
117
 
112
118
  Run `--help` at any level for details — `argus --help`, `argus claude --help`,
113
- `argus claude template --help`.
119
+ `argus claude template --help`, `argus daemon --help`.
120
+
121
+ ### `argus daemon` — background daemon (argusd)
122
+
123
+ By default the watcher and the detector scheduler run *inside* `argus start` —
124
+ close the dashboard and ingestion stops. `argusd` moves that work into a
125
+ long-running background process so your data stays fresh and detectors keep
126
+ running whether or not the dashboard is open. It does **not** serve the
127
+ dashboard (port 4242 is still only bound by `argus start`).
128
+
129
+ ```sh
130
+ argus daemon start # run argusd in the background
131
+ argus daemon status # PID + uptime
132
+ argus daemon logs -f # follow the log (survives rotation)
133
+ argus daemon stop
134
+ ```
135
+
136
+ > **Autostart at login is coming separately.** For now, start argusd yourself
137
+ > with `argus daemon start` (it survives closing the terminal). OS-native
138
+ > autostart (`argus install` / `argus uninstall`) lands in a follow-up.
139
+
140
+ **Coexistence.** When argusd is running, `argus start` notices its PID file and
141
+ becomes a **read-only dashboard** — it skips its own watcher/scheduler and just
142
+ views the database the daemon keeps fresh (the footer shows *"Powered by
143
+ argusd"*). When argusd is **not** running, `argus start` ingests in-process
144
+ exactly as before, so users who never touch the daemon see no change.
145
+
146
+ > **Known limitation (v1):** if argusd dies while a read-only dashboard is open,
147
+ > the footer keeps showing *"Powered by argusd"* and ingestion pauses until you
148
+ > restart the dashboard (which then resumes ingesting in-process). The dashboard
149
+ > does not re-check daemon liveness on every poll.
150
+
151
+ **Running it long-term.** argusd is designed to stay up. Its idle cost is
152
+ negligible — the watcher is event-driven (sleeps until a `~/.claude` file
153
+ changes) and the detector scheduler wakes once every 10 minutes; expect ~40–70
154
+ MB RAM and effectively no idle CPU. The log is capped at ~4 MB (1 MB × 3
155
+ rotations). A few habits worth knowing:
156
+
157
+ - **Restart after upgrading argus.** A running daemon holds the old code in
158
+ memory — run `argus daemon restart` after `uv sync` / `pip install -U` to pick
159
+ up changes.
160
+ - **No auto-restart yet.** A daemon started with `argus daemon start` that
161
+ crashes stays down until you start it again (the stale PID file is cleaned up
162
+ automatically). OS-supervised restart arrives with autostart in a follow-up.
163
+ - **Windows stop is a hard kill.** `argus daemon stop` terminates the process
164
+ directly on Windows, so no `argusd stopped.` line is written to the log (the
165
+ CLI still prints it). This is safe — there's no critical in-memory state.
166
+ - **Toggling search:** the dashboard's "Enable indexing" button indexes
167
+ immediately; the CLI `argus search enable` only flips the flag and defers the
168
+ backfill to the next `argus start` / `argus daemon restart`.
169
+
170
+ Logs live at `~/.argus/argusd.log` (plain text, rotated at ~1 MB × 3).
114
171
 
115
172
  ### `argus claude` — project scaffolding
116
173
 
@@ -176,6 +233,12 @@ For vulnerability reports, see [SECURITY.md](./SECURITY.md).
176
233
  Overview's "What needs attention" card reads these, and critical findings
177
234
  raise a browser notification. The v1 detector flags tools whose error rate
178
235
  spiked versus their preceding 4-week baseline.
236
+ 7. **Shared runtime + daemon.** The watcher, scheduler, and first-pass ingest
237
+ are owned by a single `CoreRuntime` that both `argus start` and `argusd`
238
+ construct. The optional `argusd` daemon (`argus daemon`) runs that runtime in
239
+ its own process; a live daemon flips `argus start`
240
+ into a read-only viewer so the two never double-ingest. See **`argus
241
+ daemon`** above.
179
242
 
180
243
  ## Configuration
181
244
 
@@ -216,7 +279,7 @@ set, Argus's own database keeps the data even after Claude rotates it out.
216
279
  git clone https://github.com/KrishBhimani/argus-code.git
217
280
  cd argus-code
218
281
  uv sync # install deps + create venv
219
- uv run pytest # ~215 tests, ~20s
282
+ uv run pytest # ~245 tests, ~20s
220
283
  uv run argus start # dev — runs directly from source
221
284
  ```
222
285
 
@@ -231,6 +294,8 @@ python/argus/ Python ingest, store, server, CLI
231
294
  store/ SQLite schema + migrations + repo
232
295
  server/ FastAPI app + /api routes
233
296
  collector/ watcher + pipeline + first-run + search backfill + alert scheduler
297
+ core/ CoreRuntime — shared watcher+scheduler+ingest lifecycle
298
+ daemon/ argusd: pidfile, foreground service, process control, logging
234
299
  detectors/ alert detectors (pure reads) + @register registry
235
300
  scaffold/ `argus claude` template storage / init / snapshot
236
301
  pricing/ LiteLLM-derived price table + cost compute
@@ -54,11 +54,68 @@ argus # top-level command group
54
54
  │ ├─ list # list templates (bundled + user)
55
55
  │ └─ create <name> [--path <dir>] [--all]
56
56
  │ # save a project's .claude/ as a template
57
+ ├─ daemon # argusd — background ingestion + detectors
58
+ │ ├─ start # start argusd detached, write PID file
59
+ │ ├─ stop # stop argusd gracefully
60
+ │ ├─ restart # stop then start
61
+ │ ├─ status # running? PID + uptime
62
+ │ └─ logs [-n N] [-f] # tail ~/.argus/argusd.log
57
63
  └─ wipe # delete ~/.argus/ entirely
58
64
  ```
59
65
 
60
66
  Run `--help` at any level for details — `argus --help`, `argus claude --help`,
61
- `argus claude template --help`.
67
+ `argus claude template --help`, `argus daemon --help`.
68
+
69
+ ### `argus daemon` — background daemon (argusd)
70
+
71
+ By default the watcher and the detector scheduler run *inside* `argus start` —
72
+ close the dashboard and ingestion stops. `argusd` moves that work into a
73
+ long-running background process so your data stays fresh and detectors keep
74
+ running whether or not the dashboard is open. It does **not** serve the
75
+ dashboard (port 4242 is still only bound by `argus start`).
76
+
77
+ ```sh
78
+ argus daemon start # run argusd in the background
79
+ argus daemon status # PID + uptime
80
+ argus daemon logs -f # follow the log (survives rotation)
81
+ argus daemon stop
82
+ ```
83
+
84
+ > **Autostart at login is coming separately.** For now, start argusd yourself
85
+ > with `argus daemon start` (it survives closing the terminal). OS-native
86
+ > autostart (`argus install` / `argus uninstall`) lands in a follow-up.
87
+
88
+ **Coexistence.** When argusd is running, `argus start` notices its PID file and
89
+ becomes a **read-only dashboard** — it skips its own watcher/scheduler and just
90
+ views the database the daemon keeps fresh (the footer shows *"Powered by
91
+ argusd"*). When argusd is **not** running, `argus start` ingests in-process
92
+ exactly as before, so users who never touch the daemon see no change.
93
+
94
+ > **Known limitation (v1):** if argusd dies while a read-only dashboard is open,
95
+ > the footer keeps showing *"Powered by argusd"* and ingestion pauses until you
96
+ > restart the dashboard (which then resumes ingesting in-process). The dashboard
97
+ > does not re-check daemon liveness on every poll.
98
+
99
+ **Running it long-term.** argusd is designed to stay up. Its idle cost is
100
+ negligible — the watcher is event-driven (sleeps until a `~/.claude` file
101
+ changes) and the detector scheduler wakes once every 10 minutes; expect ~40–70
102
+ MB RAM and effectively no idle CPU. The log is capped at ~4 MB (1 MB × 3
103
+ rotations). A few habits worth knowing:
104
+
105
+ - **Restart after upgrading argus.** A running daemon holds the old code in
106
+ memory — run `argus daemon restart` after `uv sync` / `pip install -U` to pick
107
+ up changes.
108
+ - **No auto-restart yet.** A daemon started with `argus daemon start` that
109
+ crashes stays down until you start it again (the stale PID file is cleaned up
110
+ automatically). OS-supervised restart arrives with autostart in a follow-up.
111
+ - **Windows stop is a hard kill.** `argus daemon stop` terminates the process
112
+ directly on Windows, so no `argusd stopped.` line is written to the log (the
113
+ CLI still prints it). This is safe — there's no critical in-memory state.
114
+ - **Toggling search:** the dashboard's "Enable indexing" button indexes
115
+ immediately; the CLI `argus search enable` only flips the flag and defers the
116
+ backfill to the next `argus start` / `argus daemon restart`.
117
+
118
+ Logs live at `~/.argus/argusd.log` (plain text, rotated at ~1 MB × 3).
62
119
 
63
120
  ### `argus claude` — project scaffolding
64
121
 
@@ -124,6 +181,12 @@ For vulnerability reports, see [SECURITY.md](./SECURITY.md).
124
181
  Overview's "What needs attention" card reads these, and critical findings
125
182
  raise a browser notification. The v1 detector flags tools whose error rate
126
183
  spiked versus their preceding 4-week baseline.
184
+ 7. **Shared runtime + daemon.** The watcher, scheduler, and first-pass ingest
185
+ are owned by a single `CoreRuntime` that both `argus start` and `argusd`
186
+ construct. The optional `argusd` daemon (`argus daemon`) runs that runtime in
187
+ its own process; a live daemon flips `argus start`
188
+ into a read-only viewer so the two never double-ingest. See **`argus
189
+ daemon`** above.
127
190
 
128
191
  ## Configuration
129
192
 
@@ -164,7 +227,7 @@ set, Argus's own database keeps the data even after Claude rotates it out.
164
227
  git clone https://github.com/KrishBhimani/argus-code.git
165
228
  cd argus-code
166
229
  uv sync # install deps + create venv
167
- uv run pytest # ~215 tests, ~20s
230
+ uv run pytest # ~245 tests, ~20s
168
231
  uv run argus start # dev — runs directly from source
169
232
  ```
170
233
 
@@ -179,6 +242,8 @@ python/argus/ Python ingest, store, server, CLI
179
242
  store/ SQLite schema + migrations + repo
180
243
  server/ FastAPI app + /api routes
181
244
  collector/ watcher + pipeline + first-run + search backfill + alert scheduler
245
+ core/ CoreRuntime — shared watcher+scheduler+ingest lifecycle
246
+ daemon/ argusd: pidfile, foreground service, process control, logging
182
247
  detectors/ alert detectors (pure reads) + @register registry
183
248
  scaffold/ `argus claude` template storage / init / snapshot
184
249
  pricing/ LiteLLM-derived price table + cost compute
@@ -28,6 +28,7 @@ const path = Astro.url.pathname;
28
28
  <span class="status-pill" id="ingest-status">Loading…</span>
29
29
  <span style="margin: 0 0.8rem;">·</span>
30
30
  <span id="pricing-info">Pricing —</span>
31
+ <span id="daemon-status" style="margin-left: 0.8rem; display: none;" title="Ingestion is handled by the argusd background daemon">· Powered by argusd ●</span>
31
32
  </footer>
32
33
  <script>
33
34
  async function refresh() {
@@ -56,6 +57,8 @@ const path = Astro.url.pathname;
56
57
  ip.classList.add('busy');
57
58
  }
58
59
  }
60
+ const ds = document.getElementById('daemon-status');
61
+ if (ds) ds.style.display = s.daemon === true ? 'inline' : 'none';
59
62
  } catch (e) { /* server may not be live yet */ }
60
63
  }
61
64
  refresh();
@@ -29,7 +29,7 @@ import Default from '../layouts/Default.astro';
29
29
  </div>
30
30
 
31
31
  <div class="card" style="margin-bottom:1.2rem;">
32
- <h3>Cost over time</h3>
32
+ <h3>Tokens over time</h3>
33
33
  <div id="line" class="chart"></div>
34
34
  </div>
35
35
 
@@ -39,7 +39,7 @@ import Default from '../layouts/Default.astro';
39
39
  <div id="heatmap" class="chart"></div>
40
40
  </div>
41
41
  <div class="card">
42
- <h3>Top models by cost (window)</h3>
42
+ <h3>Top models by tokens (window)</h3>
43
43
  <div id="models" class="chart"></div>
44
44
  </div>
45
45
  </div>
@@ -52,7 +52,7 @@ import Default from '../layouts/Default.astro';
52
52
  <script>
53
53
  import { api } from '../scripts/api';
54
54
  import { usd, tok, num, shortDate, shortPath, escapeHtml } from '../scripts/format';
55
- import { lineCost, calendarHeatmap, modelMix } from '../scripts/charts';
55
+ import { lineTokens, calendarHeatmap, modelMix } from '../scripts/charts';
56
56
  import { renderAlertCard, startNotificationPoll } from '../scripts/alerts';
57
57
 
58
58
  const $ = (s: string) => document.querySelector(s) as HTMLElement;
@@ -71,25 +71,23 @@ import Default from '../layouts/Default.astro';
71
71
  $('.hero-tokens')!.textContent = tok(overview.total_tokens);
72
72
  $('#hero-cost')!.textContent = usd(overview.total_cost_usd);
73
73
  $('#hero-sessions')!.textContent = num(overview.session_count);
74
- const avg = overview.session_count ? overview.total_cost_usd / overview.session_count : 0;
75
- $('#hero-avg')!.textContent = overview.session_count ? `· ~${usd(avg)} est/session` : '';
74
+ const avgTok = overview.session_count ? Math.round(overview.total_tokens / overview.session_count) : 0;
75
+ $('#hero-avg')!.textContent = overview.session_count ? `· ${tok(avgTok)} tok/session` : '';
76
76
 
77
- const days = Object.entries(overview.cost_by_day).sort().map(([day, cost]) => ({ day, cost: cost as number }));
77
+ const days = Object.entries(overview.tokens_by_day).sort().map(([day, value]) => ({ day, value: value as number }));
78
78
  lineChart?.dispose();
79
- if (days.length) lineChart = lineCost($('#line'), days);
80
- else $('#line').innerHTML = '<p class="empty">No spend in this window.</p>';
79
+ if (days.length) lineChart = lineTokens($('#line'), days);
80
+ else $('#line').innerHTML = '<p class="empty">No activity in this window.</p>';
81
81
 
82
82
  const allOv = window === 'all' ? overview : await api.overview('all');
83
- const allDays = Object.entries(allOv.cost_by_day).sort().map(([day, cost]) => ({ day, cost: cost as number }));
83
+ const allDays = Object.entries(allOv.tokens_by_day).sort().map(([day, value]) => ({ day, value: value as number }));
84
84
  heatChart?.dispose();
85
85
  heatChart = calendarHeatmap($('#heatmap'), allDays, 90);
86
86
 
87
- // Models chart: read cost_by_model from /api/overview (window-bucketed).
88
- // Previously summed s.total_cost_usd over inWindow sessions, which dumped
89
- // a session's whole-lifetime cost onto whichever model was its primary_model.
90
- const modelsList = Object.entries(overview.cost_by_model ?? {})
87
+ // Models chart: read tokens_by_model from /api/overview (window-bucketed).
88
+ const modelsList = Object.entries(overview.tokens_by_model ?? {})
91
89
  .sort((a, b) => (b[1] as number) - (a[1] as number))
92
- .map(([name, cost]) => ({ name, cost: cost as number }));
90
+ .map(([name, value]) => ({ name, value: value as number }));
93
91
  modelsChart?.dispose();
94
92
  if (modelsList.length) modelsChart = modelMix($('#models'), modelsList);
95
93
  else $('#models').innerHTML = '<p class="empty">No data.</p>';
@@ -107,7 +107,7 @@ import Default from '../layouts/Default.astro';
107
107
 
108
108
  root.innerHTML = `
109
109
  <div class="grid-cards" style="margin-bottom:1.2rem;">
110
- <div class="card kpi"><span class="kpi-label">Tokens</span><span class="kpi-value tokens">${tok(totalTokens)}</span><span class="kpi-sub">${num(totalTokens)} total</span></div>
110
+ <div class="card kpi"><span class="kpi-label">Tokens</span><span class="kpi-value tokens">${tok(totalTokens)}</span><span class="kpi-sub">${num(totalTokens)} total${subAgents?.length ? ` · incl. ${subAgents.length} sub-agent${subAgents.length === 1 ? '' : 's'}` : ''}</span></div>
111
111
  <div class="card kpi"><span class="kpi-label">Cost <span style="color:var(--text-2);font-weight:400;">(est.)</span></span><span class="kpi-value cost">~${usd(s.total_cost_usd)}</span><span class="kpi-sub">${reportedDelta}</span></div>
112
112
  <div class="card kpi"><span class="kpi-label">Turns</span><span class="kpi-value">${num(s.turn_count)}</span><span class="kpi-sub">${turns.length} loaded</span></div>
113
113
  <div class="card kpi"><span class="kpi-label">Duration</span><span class="kpi-value">${dur(s.duration_sec)}</span><span class="kpi-sub">started ${fmtLocalDateTime(s.started_at)}</span></div>
@@ -212,6 +212,17 @@ import Default from '../layouts/Default.astro';
212
212
  progress.style.display = 'none';
213
213
  }
214
214
 
215
+ // When yielded to a running argusd the API is read-only — write
216
+ // controls would 409. Show a note instead of buttons that fail.
217
+ let daemon = false;
218
+ try {
219
+ daemon = (await fetch('/api/ingest/status').then(r => r.json())).daemon === true;
220
+ } catch { /* leave daemon=false */ }
221
+ if (daemon) {
222
+ actions.innerHTML = `<span style="color:var(--text-2);font-size:0.82rem;">Read-only while <code>argusd</code> is running — manage indexing from the CLI (<code>argus search enable</code>) or stop the daemon with <code>argus daemon stop</code>.</span>`;
223
+ return;
224
+ }
225
+
215
226
  // Action buttons reflect current state. Buttons that are
216
227
  // pointless in the current state are simply omitted instead of
217
228
  // disabled — fewer affordances to mis-click.
@@ -2,6 +2,7 @@ import * as echarts from 'echarts/core';
2
2
  import { LineChart, BarChart, HeatmapChart, PieChart } from 'echarts/charts';
3
3
  import { GridComponent, TooltipComponent, TitleComponent, LegendComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
4
4
  import { CanvasRenderer } from 'echarts/renderers';
5
+ import { tok } from './format';
5
6
 
6
7
  echarts.use([LineChart, BarChart, HeatmapChart, PieChart, GridComponent, TooltipComponent, TitleComponent, LegendComponent, VisualMapComponent, MarkLineComponent, CanvasRenderer]);
7
8
 
@@ -25,16 +26,16 @@ export function makeChart(el: HTMLElement) {
25
26
  return echarts.init(el, null, { renderer: 'canvas' });
26
27
  }
27
28
 
28
- export function lineCost(el: HTMLElement, days: { day: string; cost: number }[]) {
29
+ export function lineTokens(el: HTMLElement, days: { day: string; value: number }[]) {
29
30
  const c = makeChart(el);
30
31
  c.setOption({
31
32
  ...THEME,
32
- tooltip: { ...THEME.tooltip, trigger: 'axis', valueFormatter: (v: number) => '$' + v.toFixed(2) },
33
+ tooltip: { ...THEME.tooltip, trigger: 'axis', valueFormatter: (v: number) => tok(v) + ' tokens' },
33
34
  grid: { left: 50, right: 18, top: 18, bottom: 30 },
34
35
  xAxis: { type: 'category', data: days.map(d => d.day.slice(5)), ...AXIS },
35
- yAxis: { type: 'value', axisLabel: { ...AXIS.axisLabel, formatter: '${value}' }, splitLine: AXIS.splitLine, axisLine: { show: false } },
36
+ yAxis: { type: 'value', axisLabel: { ...AXIS.axisLabel, formatter: (v: number) => tok(v) }, splitLine: AXIS.splitLine, axisLine: { show: false } },
36
37
  series: [{
37
- type: 'line', data: days.map(d => +d.cost.toFixed(4)),
38
+ type: 'line', data: days.map(d => d.value),
38
39
  smooth: true, symbol: 'circle', symbolSize: 5,
39
40
  lineStyle: { color: '#f0883e', width: 2 },
40
41
  itemStyle: { color: '#f0883e' },
@@ -65,17 +66,17 @@ export function agentSplit(el: HTMLElement, split: Record<string, { cost: number
65
66
  return c;
66
67
  }
67
68
 
68
- export function modelMix(el: HTMLElement, models: { name: string; cost: number }[]) {
69
+ export function modelMix(el: HTMLElement, models: { name: string; value: number }[]) {
69
70
  const c = makeChart(el);
70
71
  const top = models.slice(0, 8);
71
72
  c.setOption({
72
73
  ...THEME,
73
- tooltip: { ...THEME.tooltip, valueFormatter: (v: number) => '$' + v.toFixed(2) },
74
+ tooltip: { ...THEME.tooltip, valueFormatter: (v: number) => tok(v) + ' tokens' },
74
75
  grid: { left: 140, right: 30, top: 8, bottom: 24 },
75
- xAxis: { type: 'value', axisLabel: { ...AXIS.axisLabel, formatter: '${value}' }, splitLine: AXIS.splitLine, axisLine: { show: false } },
76
+ xAxis: { type: 'value', axisLabel: { ...AXIS.axisLabel, formatter: (v: number) => tok(v) }, splitLine: AXIS.splitLine, axisLine: { show: false } },
76
77
  yAxis: { type: 'category', data: top.map(m => m.name).reverse(), axisLabel: { ...AXIS.axisLabel, fontSize: 11 }, axisLine: { show: false }, axisTick: { show: false } },
77
78
  series: [{
78
- type: 'bar', data: top.map(m => +m.cost.toFixed(4)).reverse(),
79
+ type: 'bar', data: top.map(m => m.value).reverse(),
79
80
  itemStyle: { color: '#f0883e', borderRadius: [0, 4, 4, 0] },
80
81
  barWidth: '60%',
81
82
  }],
@@ -83,9 +84,9 @@ export function modelMix(el: HTMLElement, models: { name: string; cost: number }
83
84
  return c;
84
85
  }
85
86
 
86
- export function calendarHeatmap(el: HTMLElement, days: { day: string; cost: number }[], lookbackDays = 90) {
87
+ export function calendarHeatmap(el: HTMLElement, days: { day: string; value: number }[], lookbackDays = 90) {
87
88
  const c = makeChart(el);
88
- const lookup = Object.fromEntries(days.map(d => [d.day, d.cost]));
89
+ const lookup = Object.fromEntries(days.map(d => [d.day, d.value]));
89
90
  // Anchor at UTC midnight so lookup keys align with the API's started_at.slice(0,10)
90
91
  // bucketing. Stepping by exactly 86400000 ms from a UTC-midnight anchor is robust against
91
92
  // local-timezone hour-of-day skew.
@@ -131,7 +132,7 @@ export function calendarHeatmap(el: HTMLElement, days: { day: string; cost: numb
131
132
  // ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']: Sun→6, Mon→0, Tue→1, ..., Sat→5
132
133
  const rowIdx = (d.getUTCDay() + 6) % 7;
133
134
  const weekIdx = Math.round((monOfWeek(dayMs) - firstWeekMonMs) / (7 * 86_400_000));
134
- cells.push([weekIdx, rowIdx, +v.toFixed(4)]);
135
+ cells.push([weekIdx, rowIdx, v]);
135
136
  cellDate.set(`${weekIdx}-${rowIdx}`, key);
136
137
  }
137
138
  // Total columns spans first calendar week containing oldest day through
@@ -146,7 +147,7 @@ export function calendarHeatmap(el: HTMLElement, days: { day: string; cost: numb
146
147
  const dow = p.data[1];
147
148
  const v = p.data[2];
148
149
  const key = cellDate.get(`${wk}-${dow}`) ?? '';
149
- return `${key}<br>${v > 0 ? '~$' + v.toFixed(2) : '<span style="color:#6b7585;">no activity</span>'}`;
150
+ return `${key}<br>${v > 0 ? tok(v) + ' tokens' : '<span style="color:#6b7585;">no activity</span>'}`;
150
151
  },
151
152
  },
152
153
  grid: { left: 30, right: 10, top: 8, bottom: 22 },
@@ -162,7 +163,7 @@ export function calendarHeatmap(el: HTMLElement, days: { day: string; cost: numb
162
163
  splitArea: { show: false },
163
164
  },
164
165
  visualMap: {
165
- min: 0, max: Math.max(0.01, max),
166
+ min: 0, max: Math.max(1, max),
166
167
  calculable: true, orient: 'horizontal', left: 'center', bottom: 0,
167
168
  inRange: { color: ['#1c222d', '#7a3e1e', '#c2622e', '#f0883e'] },
168
169
  textStyle: { color: '#6b7585', fontSize: 10 },