agentpack-cli 0.1.15__tar.gz → 0.1.19__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 (86) hide show
  1. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/PKG-INFO +11 -7
  2. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/README.md +10 -6
  3. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/pyproject.toml +1 -1
  4. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/__init__.py +1 -1
  5. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/application/pack_service.py +12 -0
  6. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/cli.py +2 -2
  7. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/doctor.py +59 -2
  8. agentpack_cli-0.1.19/src/agentpack/commands/hook_cmd.py +223 -0
  9. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/install.py +9 -2
  10. agentpack_cli-0.1.19/src/agentpack/installers/claude.py +200 -0
  11. agentpack_cli-0.1.19/src/agentpack/mcp_server.py +415 -0
  12. agentpack_cli-0.1.15/src/agentpack/installers/claude.py +0 -172
  13. agentpack_cli-0.1.15/src/agentpack/mcp_server.py +0 -195
  14. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/.gitignore +0 -0
  15. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/LICENSE +0 -0
  16. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/adapters/__init__.py +0 -0
  17. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/adapters/antigravity.py +0 -0
  18. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/adapters/base.py +0 -0
  19. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/adapters/claude.py +0 -0
  20. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/adapters/codex.py +0 -0
  21. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/adapters/cursor.py +0 -0
  22. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/adapters/detect.py +0 -0
  23. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/adapters/generic.py +0 -0
  24. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/adapters/windsurf.py +0 -0
  25. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/__init__.py +0 -0
  26. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/dependency_graph.py +0 -0
  27. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/go_imports.py +0 -0
  28. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/java_imports.py +0 -0
  29. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/js_ts_imports.py +0 -0
  30. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/python_imports.py +0 -0
  31. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/ranking.py +0 -0
  32. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/rust_imports.py +0 -0
  33. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/symbols.py +0 -0
  34. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/analysis/tests.py +0 -0
  35. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/application/__init__.py +0 -0
  36. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/__init__.py +0 -0
  37. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/_shared.py +0 -0
  38. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/benchmark.py +0 -0
  39. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/claude_cmd.py +0 -0
  40. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/diff.py +0 -0
  41. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/explain.py +0 -0
  42. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/init.py +0 -0
  43. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/mcp_cmd.py +0 -0
  44. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/monitor.py +0 -0
  45. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/pack.py +0 -0
  46. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/scan.py +0 -0
  47. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/stats.py +0 -0
  48. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/status.py +0 -0
  49. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/summarize.py +0 -0
  50. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/commands/watch.py +0 -0
  51. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/__init__.py +0 -0
  52. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/bootstrap.py +0 -0
  53. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/cache.py +0 -0
  54. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/config.py +0 -0
  55. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/context_pack.py +0 -0
  56. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/diff.py +0 -0
  57. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/git.py +0 -0
  58. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/git_hooks.py +0 -0
  59. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/global_install.py +0 -0
  60. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/ignore.py +0 -0
  61. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/merkle.py +0 -0
  62. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/models.py +0 -0
  63. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/redactor.py +0 -0
  64. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/scanner.py +0 -0
  65. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/snapshot.py +0 -0
  66. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/token_estimator.py +0 -0
  67. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/core/vscode_tasks.py +0 -0
  68. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/data/agentpack.md +0 -0
  69. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/installers/__init__.py +0 -0
  70. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/installers/antigravity.py +0 -0
  71. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/installers/codex.py +0 -0
  72. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/installers/cursor.py +0 -0
  73. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/installers/windsurf.py +0 -0
  74. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/integrations/__init__.py +0 -0
  75. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/integrations/git_hooks.py +0 -0
  76. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/integrations/global_install.py +0 -0
  77. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/integrations/vscode_tasks.py +0 -0
  78. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/renderers/__init__.py +0 -0
  79. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/renderers/compact.py +0 -0
  80. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/renderers/markdown.py +0 -0
  81. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/renderers/receipts.py +0 -0
  82. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/session/__init__.py +0 -0
  83. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/session/state.py +0 -0
  84. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/summaries/__init__.py +0 -0
  85. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/summaries/base.py +0 -0
  86. {agentpack_cli-0.1.15 → agentpack_cli-0.1.19}/src/agentpack/summaries/offline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentpack-cli
3
- Version: 0.1.15
3
+ Version: 0.1.19
4
4
  Summary: Token-aware context packing for AI coding agents — Claude, Cursor, Windsurf, and Codex
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -44,7 +44,7 @@ Description-Content-Type: text/markdown
44
44
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
45
45
  [![CI](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml/badge.svg)](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
46
46
 
47
- > **Status: alpha (v0.1.11).** Works, tested, used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Not yet validated across a wide range of repos. API may change before 1.0.
47
+ > **Status: alpha (v0.1.19).** Works, tested, used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Not yet validated across a wide range of repos. API may change before 1.0.
48
48
  >
49
49
  > **Platform note:** macOS and Linux are fully supported. Windows support is not yet implemented (git hooks use POSIX shell; the Claude Code session hooks use `python3`/`rm -f`). Contributions welcome.
50
50
 
@@ -297,7 +297,7 @@ Configures:
297
297
  - `CLAUDE.md` — tells Claude to read the context pack before each task
298
298
  - `.claude/settings.json` — two hooks:
299
299
  - `SessionStart`: clears injection sentinel so first prompt gets context
300
- - `UserPromptSubmit`: auto-repacks when stale, injects context once per session
300
+ - `UserPromptSubmit`: runs `agentpack hook` — detects repo changes via `root_hash`, triggers background repack using your prompt as task. With MCP: emits Option-B hint (~100 tokens, task + top files). Without MCP: emits capped fallback (top 8 files, ≤3k chars)
301
301
 
302
302
  After this, context is injected automatically into every Claude Code session. No `/agentpack` command needed — it just happens.
303
303
 
@@ -354,7 +354,7 @@ The Skill descriptor activates AgentPack automatically — no `--task` flag requ
354
354
  |---|---|---|---|---|---|
355
355
  | Config file patched | `CLAUDE.md` + `.claude/settings.json` | `.cursorrules` + `.cursor/rules/*.mdc` | `.windsurfrules` | `AGENTS.md` | `.agent/skills/agentpack/SKILL.md` + `GEMINI.md` |
356
356
  | Auto-inject on startup | ✅ `UserPromptSubmit` hook | ✅ `alwaysApply` | ✅ rules file | ✅ `AGENTS.md` | ✅ Skill auto-activation |
357
- | Auto-repack when stale | ✅ hook (snapshot hash, ~1ms when fresh) | ✅ git hooks | ✅ git hooks | ✅ git hooks | ✅ git hooks |
357
+ | Auto-repack when stale | ✅ hook (content hash via `root_hash`, ~1ms when fresh) | ✅ git hooks | ✅ git hooks | ✅ git hooks | ✅ git hooks |
358
358
  | Manual repack shortcut | ✅ `/agentpack` slash cmd | ✅ VS Code task | ✅ VS Code task | `agentpack pack` | ✅ VS Code task |
359
359
 
360
360
  ---
@@ -640,7 +640,7 @@ Options:
640
640
 
641
641
  ### `agentpack session` _(removed)_
642
642
 
643
- Session management was removed in v0.1.11. `agentpack init` bootstraps the session automatically. Use `agentpack watch` to keep context current. To change the task, edit `.agentpack/task.md`.
643
+ Session management was removed in v0.1.12. `agentpack init` bootstraps the session automatically. Use `agentpack watch` to keep context current. To change the task, edit `.agentpack/task.md`.
644
644
 
645
645
  ---
646
646
 
@@ -826,13 +826,13 @@ Tokens after ignore: 210,000
826
826
 
827
827
  ### `agentpack stats`
828
828
 
829
- Show session state and token statistics for the last pack.
829
+ Show session state, token statistics, and selection accuracy for the last pack.
830
830
 
831
831
  ```bash
832
832
  agentpack stats
833
833
  ```
834
834
 
835
- When a session is active, shows session panel (agent, mode, started, refresh count) above token stats. Also lists top included files by score.
835
+ When a session is active, shows session panel (agent, mode, started, refresh count) above token stats. Also lists top included files by score and avg recall/precision/F1 over the last 10 runs.
836
836
 
837
837
  ---
838
838
 
@@ -910,8 +910,10 @@ agentpack monitor --clear
910
910
  | Direct dependency of changed file | +50 |
911
911
  | Reverse dependency | +40 |
912
912
  | Has related tests | +35 |
913
+ | Knowledge/architecture doc (DECISIONS.md, ADR-*.md, ARCHITECTURE.md, docs/adr/, docs/decisions/, docs/rfcs/) | +30 |
913
914
  | Config file | +25 |
914
915
  | Recently modified | +20 |
916
+ | High churn (top 10% by commit frequency) | +15 |
915
917
  | Large unrelated file | −50 |
916
918
  | Ignored/binary | −100 |
917
919
 
@@ -960,8 +962,10 @@ content_keyword_max = 60
960
962
  direct_dep = 50
961
963
  reverse_dep = 40
962
964
  related_test = 35
965
+ knowledge_file = 30 # DECISIONS.md, ADR-*.md, ARCHITECTURE.md, docs/adr/ etc.
963
966
  config_file = 25
964
967
  recently_modified = 20
968
+ churn_high = 15 # top 10% by commit frequency
965
969
  large_unrelated_penalty = -50
966
970
  ignored_penalty = -100
967
971
  ```
@@ -5,7 +5,7 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![CI](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml/badge.svg)](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
7
7
 
8
- > **Status: alpha (v0.1.11).** Works, tested, used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Not yet validated across a wide range of repos. API may change before 1.0.
8
+ > **Status: alpha (v0.1.19).** Works, tested, used in real sessions. Python and JavaScript/TypeScript are the best-supported languages. Not yet validated across a wide range of repos. API may change before 1.0.
9
9
  >
10
10
  > **Platform note:** macOS and Linux are fully supported. Windows support is not yet implemented (git hooks use POSIX shell; the Claude Code session hooks use `python3`/`rm -f`). Contributions welcome.
11
11
 
@@ -258,7 +258,7 @@ Configures:
258
258
  - `CLAUDE.md` — tells Claude to read the context pack before each task
259
259
  - `.claude/settings.json` — two hooks:
260
260
  - `SessionStart`: clears injection sentinel so first prompt gets context
261
- - `UserPromptSubmit`: auto-repacks when stale, injects context once per session
261
+ - `UserPromptSubmit`: runs `agentpack hook` — detects repo changes via `root_hash`, triggers background repack using your prompt as task. With MCP: emits Option-B hint (~100 tokens, task + top files). Without MCP: emits capped fallback (top 8 files, ≤3k chars)
262
262
 
263
263
  After this, context is injected automatically into every Claude Code session. No `/agentpack` command needed — it just happens.
264
264
 
@@ -315,7 +315,7 @@ The Skill descriptor activates AgentPack automatically — no `--task` flag requ
315
315
  |---|---|---|---|---|---|
316
316
  | Config file patched | `CLAUDE.md` + `.claude/settings.json` | `.cursorrules` + `.cursor/rules/*.mdc` | `.windsurfrules` | `AGENTS.md` | `.agent/skills/agentpack/SKILL.md` + `GEMINI.md` |
317
317
  | Auto-inject on startup | ✅ `UserPromptSubmit` hook | ✅ `alwaysApply` | ✅ rules file | ✅ `AGENTS.md` | ✅ Skill auto-activation |
318
- | Auto-repack when stale | ✅ hook (snapshot hash, ~1ms when fresh) | ✅ git hooks | ✅ git hooks | ✅ git hooks | ✅ git hooks |
318
+ | Auto-repack when stale | ✅ hook (content hash via `root_hash`, ~1ms when fresh) | ✅ git hooks | ✅ git hooks | ✅ git hooks | ✅ git hooks |
319
319
  | Manual repack shortcut | ✅ `/agentpack` slash cmd | ✅ VS Code task | ✅ VS Code task | `agentpack pack` | ✅ VS Code task |
320
320
 
321
321
  ---
@@ -601,7 +601,7 @@ Options:
601
601
 
602
602
  ### `agentpack session` _(removed)_
603
603
 
604
- Session management was removed in v0.1.11. `agentpack init` bootstraps the session automatically. Use `agentpack watch` to keep context current. To change the task, edit `.agentpack/task.md`.
604
+ Session management was removed in v0.1.12. `agentpack init` bootstraps the session automatically. Use `agentpack watch` to keep context current. To change the task, edit `.agentpack/task.md`.
605
605
 
606
606
  ---
607
607
 
@@ -787,13 +787,13 @@ Tokens after ignore: 210,000
787
787
 
788
788
  ### `agentpack stats`
789
789
 
790
- Show session state and token statistics for the last pack.
790
+ Show session state, token statistics, and selection accuracy for the last pack.
791
791
 
792
792
  ```bash
793
793
  agentpack stats
794
794
  ```
795
795
 
796
- When a session is active, shows session panel (agent, mode, started, refresh count) above token stats. Also lists top included files by score.
796
+ When a session is active, shows session panel (agent, mode, started, refresh count) above token stats. Also lists top included files by score and avg recall/precision/F1 over the last 10 runs.
797
797
 
798
798
  ---
799
799
 
@@ -871,8 +871,10 @@ agentpack monitor --clear
871
871
  | Direct dependency of changed file | +50 |
872
872
  | Reverse dependency | +40 |
873
873
  | Has related tests | +35 |
874
+ | Knowledge/architecture doc (DECISIONS.md, ADR-*.md, ARCHITECTURE.md, docs/adr/, docs/decisions/, docs/rfcs/) | +30 |
874
875
  | Config file | +25 |
875
876
  | Recently modified | +20 |
877
+ | High churn (top 10% by commit frequency) | +15 |
876
878
  | Large unrelated file | −50 |
877
879
  | Ignored/binary | −100 |
878
880
 
@@ -921,8 +923,10 @@ content_keyword_max = 60
921
923
  direct_dep = 50
922
924
  reverse_dep = 40
923
925
  related_test = 35
926
+ knowledge_file = 30 # DECISIONS.md, ADR-*.md, ARCHITECTURE.md, docs/adr/ etc.
924
927
  config_file = 25
925
928
  recently_modified = 20
929
+ churn_high = 15 # top 10% by commit frequency
926
930
  large_unrelated_penalty = -50
927
931
  ignored_penalty = -100
928
932
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentpack-cli"
3
- version = "0.1.15"
3
+ version = "0.1.19"
4
4
  description = "Token-aware context packing for AI coding agents — Claude, Cursor, Windsurf, and Codex"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """AgentPack — token-aware context packing for AI coding agents."""
2
2
 
3
- __version__ = "0.1.14"
3
+ __version__ = "0.1.19"
@@ -303,6 +303,9 @@ class PackService:
303
303
  budget=plan.budget,
304
304
  token_estimate=packed_tokens,
305
305
  )
306
+ excluded_receipts = [r for r in plan.receipts if r.action == "excluded"]
307
+ # Budget-cut: files that scored OK but didn't fit — more useful signal than "score too low"
308
+ budget_cut = [r.path for r in plan.receipts if r.reason == "budget exhausted"][:10]
306
309
  _record_metrics(
307
310
  root,
308
311
  task=request.task,
@@ -314,7 +317,10 @@ class PackService:
314
317
  selected_count=len(plan.selected),
315
318
  changed_count=len(plan.all_changed),
316
319
  selected_paths=[sf.path for sf in plan.selected],
320
+ selected_hints=[{"path": sf.path, "why": sf.reasons[0] if sf.reasons else ""} for sf in plan.selected[:8]],
317
321
  current_changed=plan.all_changed,
322
+ excluded_count=len(excluded_receipts),
323
+ excluded_paths=budget_cut,
318
324
  )
319
325
 
320
326
  return PackResult(
@@ -404,6 +410,9 @@ def _record_metrics(
404
410
  changed_count: int,
405
411
  selected_paths: list[str],
406
412
  current_changed: set[str],
413
+ selected_hints: list[dict] | None = None,
414
+ excluded_count: int = 0,
415
+ excluded_paths: list[str] | None = None,
407
416
  ) -> None:
408
417
  metrics_path = root / ".agentpack" / "metrics.jsonl"
409
418
  accuracy = _compute_selection_accuracy(root, metrics_path, selected_paths, current_changed)
@@ -416,7 +425,10 @@ def _record_metrics(
416
425
  "saving_pct": round(saving_pct, 1),
417
426
  "selected_files": selected_count,
418
427
  "changed_files": changed_count,
428
+ "excluded_files": excluded_count,
429
+ "excluded_paths": excluded_paths or [],
419
430
  "selected_paths": selected_paths,
431
+ "selected_hints": selected_hints or [],
420
432
  "phases": {k: round(v, 3) for k, v in phase_times.items()},
421
433
  "total_s": round(sum(phase_times.values()), 3),
422
434
  }
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import typer
4
- from agentpack.commands import init, scan, diff, status, stats, summarize, pack, install, monitor, explain, doctor, watch, claude_cmd, benchmark, mcp_cmd
4
+ from agentpack.commands import init, scan, diff, status, stats, summarize, pack, install, monitor, explain, doctor, watch, claude_cmd, benchmark, mcp_cmd, hook_cmd
5
5
  from agentpack import __version__
6
6
 
7
7
 
@@ -21,7 +21,7 @@ def _main(
21
21
  pass
22
22
 
23
23
 
24
- for mod in [init, scan, diff, status, stats, summarize, pack, install, monitor, explain, doctor, watch, claude_cmd, benchmark, mcp_cmd]:
24
+ for mod in [init, scan, diff, status, stats, summarize, pack, install, monitor, explain, doctor, watch, claude_cmd, benchmark, mcp_cmd, hook_cmd]:
25
25
  mod.register(app)
26
26
 
27
27
 
@@ -119,12 +119,37 @@ def register(app: typer.Typer) -> None:
119
119
  import json as _json
120
120
  _local_has_hooks = False
121
121
  _global_has_hooks = False
122
+
123
+ def _has_stale_hooks(hooks: dict) -> bool:
124
+ """Detect old inline-Python or context-injection hooks that should be upgraded."""
125
+ all_cmds = [
126
+ h.get("command", "")
127
+ for event_hooks in hooks.values()
128
+ for entry in event_hooks
129
+ for h in entry.get("hooks", [])
130
+ ]
131
+ return any(
132
+ "context.claude.md" in cmd
133
+ or ".context_injected" in cmd
134
+ or (".mcp_reminded" in cmd and "python3" in cmd)
135
+ for cmd in all_cmds
136
+ )
137
+
138
+ def _has_current_hooks(hooks: dict) -> bool:
139
+ return "agentpack hook" in str(hooks)
140
+
122
141
  if claude_settings.exists():
123
142
  try:
124
143
  data = _json.loads(claude_settings.read_text())
125
144
  hooks = data.get("hooks", {})
126
145
  if "UserPromptSubmit" in hooks or "SessionStart" in hooks:
127
- console.print(f" [green]✓[/] Claude hooks present (local): {claude_settings}")
146
+ if _has_stale_hooks(hooks):
147
+ console.print(" [yellow]![/] Claude hooks stale (local) — old injection hook detected. Run: agentpack install --agent claude")
148
+ ok = False
149
+ elif _has_current_hooks(hooks):
150
+ console.print(f" [green]✓[/] Claude hooks present (local): {claude_settings}")
151
+ else:
152
+ console.print(f" [green]✓[/] Claude hooks present (local): {claude_settings}")
128
153
  _local_has_hooks = True
129
154
  else:
130
155
  console.print(" [yellow]![/] Claude hooks missing (local) — run: agentpack install --agent claude")
@@ -138,7 +163,10 @@ def register(app: typer.Typer) -> None:
138
163
  data = _json.loads(global_claude_settings.read_text())
139
164
  hooks = data.get("hooks", {})
140
165
  if "UserPromptSubmit" in hooks or "SessionStart" in hooks:
141
- console.print(f" [green]✓[/] Claude hooks present (global): {global_claude_settings}")
166
+ if _has_stale_hooks(hooks):
167
+ console.print(" [yellow]![/] Claude hooks stale (global) — old injection hook detected. Run: agentpack install --agent claude --global")
168
+ else:
169
+ console.print(f" [green]✓[/] Claude hooks present (global): {global_claude_settings}")
142
170
  _global_has_hooks = True
143
171
  else:
144
172
  console.print(" [yellow]![/] Claude hooks missing (global) — run: agentpack install --agent claude --global")
@@ -149,6 +177,35 @@ def register(app: typer.Typer) -> None:
149
177
  if _local_has_hooks and not _global_has_hooks:
150
178
  console.print(" [yellow]![/] Hooks local-only — context won't auto-inject in other repos. Run: agentpack install --agent claude --global")
151
179
 
180
+ # --- MCP server ---
181
+ console.print("\n[bold]MCP server[/]")
182
+ mcp_json = root / ".mcp.json"
183
+ global_claude_settings_for_mcp = Path.home() / ".claude" / "settings.json"
184
+ _local_has_mcp = False
185
+ _global_has_mcp = False
186
+ if mcp_json.exists():
187
+ try:
188
+ mcp_data = _json.loads(mcp_json.read_text())
189
+ if "agentpack" in mcp_data.get("mcpServers", {}):
190
+ console.print(f" [green]✓[/] MCP server registered (local): {mcp_json}")
191
+ _local_has_mcp = True
192
+ else:
193
+ console.print(" [yellow]![/] .mcp.json exists but agentpack missing — run: agentpack install --agent claude")
194
+ except Exception:
195
+ console.print(f" [yellow]![/] Could not parse {mcp_json}")
196
+ else:
197
+ console.print(" [dim]-[/] .mcp.json not present (run: agentpack install --agent claude)")
198
+ if global_claude_settings_for_mcp.exists():
199
+ try:
200
+ global_data = _json.loads(global_claude_settings_for_mcp.read_text())
201
+ if "agentpack" in global_data.get("mcpServers", {}):
202
+ console.print(f" [green]✓[/] MCP server registered (global): {global_claude_settings_for_mcp}")
203
+ _global_has_mcp = True
204
+ except Exception:
205
+ pass
206
+ if not _local_has_mcp and not _global_has_mcp:
207
+ console.print(" [yellow]![/] MCP server not registered — mcp__agentpack__* tools unavailable")
208
+
152
209
  # --- Slash command ---
153
210
  console.print("\n[bold]Slash command (/agentpack)[/]")
154
211
  local_cmd = root / ".claude" / "commands" / "agentpack.md"
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from agentpack.commands._shared import _root
11
+
12
+ _TASK_FILE = ".agentpack/task.md"
13
+ _TASK_FILE_DEFAULT_MARKER = "Write or update the current coding task here."
14
+
15
+
16
+ def register(app: typer.Typer) -> None:
17
+ @app.command(name="hook")
18
+ def hook(
19
+ event: str = typer.Option("UserPromptSubmit", "--event", help="Hook event name."),
20
+ ) -> None:
21
+ """Run as a Claude Code hook. Reads stdin (JSON), emits additionalContext."""
22
+ root = _root()
23
+ if event == "UserPromptSubmit":
24
+ _run_user_prompt_submit(root)
25
+ elif event == "SessionStart":
26
+ _run_session_start(root)
27
+ else:
28
+ sys.exit(0)
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Public helpers (tested directly)
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def _mcp_installed(root: Path) -> bool:
36
+ local_mcp = root / ".mcp.json"
37
+ if local_mcp.exists():
38
+ try:
39
+ cfg = json.loads(local_mcp.read_text())
40
+ if "agentpack" in cfg.get("mcpServers", {}):
41
+ return True
42
+ except Exception:
43
+ pass
44
+ global_settings = Path.home() / ".claude" / "settings.json"
45
+ if global_settings.exists():
46
+ try:
47
+ cfg = json.loads(global_settings.read_text())
48
+ if "agentpack" in cfg.get("mcpServers", {}):
49
+ return True
50
+ except Exception:
51
+ pass
52
+ return False
53
+
54
+
55
+ def _load_task_md(root: Path) -> str:
56
+ """Return task.md content if user has written a real task (not the default placeholder)."""
57
+ task_path = root / _TASK_FILE
58
+ if not task_path.exists():
59
+ return ""
60
+ try:
61
+ content = task_path.read_text(encoding="utf-8").strip()
62
+ # Strip markdown heading
63
+ lines = [ln for ln in content.splitlines() if not ln.startswith("#")]
64
+ body = "\n".join(lines).strip()
65
+ if not body or _TASK_FILE_DEFAULT_MARKER in body:
66
+ return ""
67
+ return body[:200]
68
+ except Exception:
69
+ return ""
70
+
71
+
72
+ def _resolve_task(root: Path, prompt: str) -> str:
73
+ """Merge task.md + prompt into best task description for repack."""
74
+ task_md = _load_task_md(root)
75
+ if task_md:
76
+ return task_md
77
+ return prompt[:200].strip() if prompt else "auto"
78
+
79
+
80
+ def _load_hints(root: Path, n: int = 5) -> list[dict]:
81
+ """Return top-n selected_hints (path + why) from last metrics record."""
82
+ metrics_path = root / ".agentpack" / "metrics.jsonl"
83
+ if not metrics_path.exists():
84
+ return []
85
+ try:
86
+ lines = metrics_path.read_text(encoding="utf-8").splitlines()
87
+ for line in reversed(lines):
88
+ line = line.strip()
89
+ if not line:
90
+ continue
91
+ rec = json.loads(line)
92
+ hints = rec.get("selected_hints", [])
93
+ if hints:
94
+ return hints[:n]
95
+ # Fallback: old metrics without hints
96
+ paths = rec.get("selected_paths", [])
97
+ if paths:
98
+ return [{"path": p, "why": ""} for p in paths[:n]]
99
+ except Exception:
100
+ pass
101
+ return []
102
+
103
+
104
+ def _load_top_files(root: Path, n: int = 5) -> list[dict]:
105
+ """Alias kept for backward compat with tests."""
106
+ return _load_hints(root, n)
107
+
108
+
109
+ def _load_pack_task(root: Path) -> str:
110
+ meta_path = root / ".agentpack" / "pack_metadata.json"
111
+ if not meta_path.exists():
112
+ return ""
113
+ try:
114
+ return json.loads(meta_path.read_text()).get("task", "")
115
+ except Exception:
116
+ return ""
117
+
118
+
119
+ def _current_root_hash(root: Path) -> str | None:
120
+ snap = root / ".agentpack" / "snapshots" / "latest.json"
121
+ if not snap.exists():
122
+ return None
123
+ try:
124
+ return json.loads(snap.read_text()).get("root_hash")
125
+ except Exception:
126
+ return None
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Event handlers
131
+ # ---------------------------------------------------------------------------
132
+
133
+ def _run_session_start(root: Path) -> None:
134
+ """Clear sentinels so first prompt gets fresh context."""
135
+ for sentinel in [
136
+ root / ".agentpack" / ".mcp_reminded",
137
+ root / ".agentpack" / ".context_injected",
138
+ ]:
139
+ try:
140
+ sentinel.unlink(missing_ok=True)
141
+ except Exception:
142
+ pass
143
+ # No output needed — SessionStart hooks don't inject additionalContext
144
+
145
+
146
+ def _run_user_prompt_submit(root: Path) -> None:
147
+ snap_sentinel = root / ".agentpack" / ".mcp_reminded"
148
+
149
+ try:
150
+ hook_data = json.loads(sys.stdin.read())
151
+ prompt = hook_data.get("prompt", "")
152
+ except Exception:
153
+ prompt = ""
154
+
155
+ task = _resolve_task(root, prompt)
156
+
157
+ current_hash = _current_root_hash(root)
158
+ reminded_hash = snap_sentinel.read_text().strip() if snap_sentinel.exists() else None
159
+ repo_changed = current_hash != reminded_hash
160
+
161
+ if repo_changed:
162
+ subprocess.Popen(
163
+ ["agentpack", "pack", "--task", task, "--mode", "balanced", "--since", "HEAD~1"],
164
+ stdout=subprocess.DEVNULL,
165
+ stderr=subprocess.DEVNULL,
166
+ )
167
+ try:
168
+ snap_sentinel.write_text(current_hash or "1")
169
+ except Exception:
170
+ pass
171
+
172
+ has_mcp = _mcp_installed(root)
173
+
174
+ if has_mcp:
175
+ hints = _load_hints(root, n=5)
176
+ if hints:
177
+ files_lines = "\n".join(
178
+ f" - {h['path']}" + (f" — {h['why']}" if h.get("why") else "")
179
+ for h in hints
180
+ )
181
+ status_note = "(repacking — call pack_context for fresh results)" if repo_changed else "(index fresh)"
182
+ current_task = _load_task_md(root) or _load_pack_task(root) or "unknown"
183
+ msg = (
184
+ f"AgentPack {status_note}\n"
185
+ f"task: {current_task}\n"
186
+ f"top files:\n{files_lines}\n"
187
+ f"Call agentpack_pack_context(task=\"...\") for full ranked context."
188
+ )
189
+ else:
190
+ msg = (
191
+ "AgentPack active. No pack yet — call agentpack_pack_context(task=\"...\") "
192
+ "to build context for this task."
193
+ )
194
+ else:
195
+ hints = _load_hints(root, n=8)
196
+ current_task = _load_task_md(root) or _load_pack_task(root) or "unknown"
197
+ if hints:
198
+ files_lines = "\n".join(
199
+ f" - {h['path']}" + (f" — {h['why']}" if h.get("why") else "")
200
+ for h in hints
201
+ )
202
+ changed_note = " (repacking in background)" if repo_changed else ""
203
+ msg = (
204
+ f"AgentPack context{changed_note}\n"
205
+ f"task: {current_task}\n"
206
+ f"top files:\n{files_lines}\n\n"
207
+ f"For richer context, install MCP: agentpack install --agent claude"
208
+ )
209
+ else:
210
+ msg = (
211
+ "AgentPack active. Run `agentpack pack --task \"<task>\"` to build context.\n"
212
+ "For auto context, install MCP: agentpack install --agent claude"
213
+ )
214
+
215
+ if len(msg) > 3000:
216
+ msg = msg[:2970] + "\n... [truncated]"
217
+
218
+ print(json.dumps({
219
+ "hookSpecificOutput": {
220
+ "hookEventName": "UserPromptSubmit",
221
+ "additionalContext": msg,
222
+ }
223
+ }))
@@ -51,6 +51,10 @@ def register(app: typer.Typer) -> None:
51
51
  scope = "~/.claude/settings.json" if global_install else ".claude/settings.json"
52
52
  console.print(f"[green]{scope} {hook_action}.[/]")
53
53
 
54
+ mcp_action = installer.patch_mcp_server(root, global_install)
55
+ mcp_scope = "~/.claude/settings.json" if global_install else ".mcp.json"
56
+ console.print(f"[green]{mcp_scope} mcpServers.agentpack {mcp_action}.[/]")
57
+
54
58
  if slash_command:
55
59
  _install_slash_command(root, global_install)
56
60
 
@@ -164,11 +168,14 @@ def register(app: typer.Typer) -> None:
164
168
  # --- Agent-specific config ---
165
169
  if agent == "claude":
166
170
  if not dry_run:
167
- hook_action = ClaudeInstaller().patch_claude_settings(root, global_install=True)
171
+ inst = ClaudeInstaller()
172
+ hook_action = inst.patch_claude_settings(root, global_install=True)
168
173
  console.print(f"\n[green]~/.claude/settings.json {hook_action}.[/]")
174
+ mcp_action = inst.patch_mcp_server(root, global_install=True)
175
+ console.print(f"[green]~/.claude/settings.json mcpServers.agentpack {mcp_action}.[/]")
169
176
  _install_slash_command(root, global_install=True)
170
177
  else:
171
- console.print("\n[dim]Would patch: ~/.claude/settings.json (hooks)[/]")
178
+ console.print("\n[dim]Would patch: ~/.claude/settings.json (hooks + mcpServers for global)[/]")
172
179
  console.print("[dim]Would install: ~/.claude/commands/agentpack.md (slash command)[/]")
173
180
 
174
181
  elif agent == "cursor":