agentpack-cli 0.1.16__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.
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/PKG-INFO +3 -3
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/README.md +2 -2
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/pyproject.toml +1 -1
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/__init__.py +1 -1
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/application/pack_service.py +6 -1
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/cli.py +2 -2
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/doctor.py +30 -2
- agentpack_cli-0.1.19/src/agentpack/commands/hook_cmd.py +223 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/installers/claude.py +22 -51
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/.gitignore +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/LICENSE +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/adapters/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/adapters/antigravity.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/adapters/base.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/adapters/claude.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/adapters/codex.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/adapters/cursor.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/adapters/detect.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/adapters/generic.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/adapters/windsurf.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/dependency_graph.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/go_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/java_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/js_ts_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/python_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/ranking.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/rust_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/symbols.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/analysis/tests.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/application/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/_shared.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/benchmark.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/claude_cmd.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/diff.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/explain.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/init.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/install.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/mcp_cmd.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/monitor.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/pack.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/scan.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/stats.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/status.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/summarize.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/commands/watch.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/bootstrap.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/cache.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/config.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/context_pack.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/diff.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/git.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/git_hooks.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/global_install.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/ignore.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/merkle.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/models.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/redactor.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/scanner.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/snapshot.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/token_estimator.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/core/vscode_tasks.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/data/agentpack.md +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/installers/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/installers/antigravity.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/installers/codex.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/installers/cursor.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/installers/windsurf.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/integrations/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/integrations/git_hooks.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/integrations/global_install.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/integrations/vscode_tasks.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/mcp_server.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/renderers/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/renderers/compact.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/renderers/markdown.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/renderers/receipts.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/session/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/session/state.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/summaries/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.19}/src/agentpack/summaries/base.py +0 -0
- {agentpack_cli-0.1.16 → 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.
|
|
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
|
[](https://opensource.org/licenses/MIT)
|
|
45
45
|
[](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
|
|
46
46
|
|
|
47
|
-
> **Status: alpha (v0.1.
|
|
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`: detects repo changes via
|
|
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
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
|
|
7
7
|
|
|
8
|
-
> **Status: alpha (v0.1.
|
|
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`: detects repo changes via
|
|
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
|
|
|
@@ -304,6 +304,8 @@ class PackService:
|
|
|
304
304
|
token_estimate=packed_tokens,
|
|
305
305
|
)
|
|
306
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]
|
|
307
309
|
_record_metrics(
|
|
308
310
|
root,
|
|
309
311
|
task=request.task,
|
|
@@ -315,9 +317,10 @@ class PackService:
|
|
|
315
317
|
selected_count=len(plan.selected),
|
|
316
318
|
changed_count=len(plan.all_changed),
|
|
317
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]],
|
|
318
321
|
current_changed=plan.all_changed,
|
|
319
322
|
excluded_count=len(excluded_receipts),
|
|
320
|
-
excluded_paths=
|
|
323
|
+
excluded_paths=budget_cut,
|
|
321
324
|
)
|
|
322
325
|
|
|
323
326
|
return PackResult(
|
|
@@ -407,6 +410,7 @@ def _record_metrics(
|
|
|
407
410
|
changed_count: int,
|
|
408
411
|
selected_paths: list[str],
|
|
409
412
|
current_changed: set[str],
|
|
413
|
+
selected_hints: list[dict] | None = None,
|
|
410
414
|
excluded_count: int = 0,
|
|
411
415
|
excluded_paths: list[str] | None = None,
|
|
412
416
|
) -> None:
|
|
@@ -424,6 +428,7 @@ def _record_metrics(
|
|
|
424
428
|
"excluded_files": excluded_count,
|
|
425
429
|
"excluded_paths": excluded_paths or [],
|
|
426
430
|
"selected_paths": selected_paths,
|
|
431
|
+
"selected_hints": selected_hints or [],
|
|
427
432
|
"phases": {k: round(v, 3) for k, v in phase_times.items()},
|
|
428
433
|
"total_s": round(sum(phase_times.values()), 3),
|
|
429
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
|
-
|
|
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
|
-
|
|
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")
|
|
@@ -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
|
+
}))
|
|
@@ -77,70 +77,41 @@ class ClaudeInstaller:
|
|
|
77
77
|
|
|
78
78
|
hooks = existing.setdefault("hooks", {})
|
|
79
79
|
|
|
80
|
-
# SessionStart:
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
sentinel_cmd = (
|
|
84
|
-
"rm -f .agentpack/.context_injected .agentpack/.mcp_reminded"
|
|
85
|
-
" && ([ -f .agentpack/session.json ]"
|
|
86
|
-
" && agentpack session refresh >/dev/null 2>&1"
|
|
87
|
-
" || agentpack pack --task auto --mode balanced >/dev/null 2>&1) &"
|
|
88
|
-
)
|
|
80
|
+
# SessionStart: delegate to `agentpack hook` CLI subcommand.
|
|
81
|
+
# Clears sentinels so first UserPromptSubmit gets fresh context.
|
|
82
|
+
session_hook_cmd = "agentpack hook --event SessionStart"
|
|
89
83
|
session_start = hooks.setdefault("SessionStart", [])
|
|
90
|
-
#
|
|
84
|
+
# Remove stale agentpack session hooks (old rm -f / session refresh shell commands).
|
|
85
|
+
def _is_stale_session_hook(cmd: str) -> bool:
|
|
86
|
+
return (
|
|
87
|
+
".context_injected" in cmd and "rm -f" in cmd
|
|
88
|
+
) or "agentpack session refresh" in cmd
|
|
91
89
|
for entry in session_start:
|
|
92
90
|
entry["hooks"] = [
|
|
93
91
|
h for h in entry.get("hooks", [])
|
|
94
|
-
if not (
|
|
92
|
+
if not _is_stale_session_hook(h.get("command", ""))
|
|
95
93
|
]
|
|
96
94
|
session_start[:] = [e for e in session_start if e.get("hooks")]
|
|
97
95
|
already_has_session_hook = any(
|
|
98
|
-
any(h.get("command", "") ==
|
|
96
|
+
any(h.get("command", "") == session_hook_cmd for h in entry.get("hooks", []))
|
|
99
97
|
for entry in session_start
|
|
100
98
|
)
|
|
101
99
|
if not already_has_session_hook:
|
|
102
|
-
session_start.append({"hooks": [{"type": "command", "command":
|
|
103
|
-
|
|
104
|
-
# UserPromptSubmit:
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
"python3 -c \"\n"
|
|
111
|
-
"import json, pathlib, subprocess, sys\n"
|
|
112
|
-
"snap = pathlib.Path('.agentpack/snapshots/latest.json')\n"
|
|
113
|
-
"sentinel = pathlib.Path('.agentpack/.mcp_reminded')\n"
|
|
114
|
-
"try:\n"
|
|
115
|
-
" current_hash = json.loads(snap.read_text()).get('root_hash') if snap.exists() else None\n"
|
|
116
|
-
"except Exception:\n"
|
|
117
|
-
" current_hash = None\n"
|
|
118
|
-
"reminded_hash = sentinel.read_text().strip() if sentinel.exists() else None\n"
|
|
119
|
-
"try:\n"
|
|
120
|
-
" hook_data = json.loads(sys.stdin.read())\n"
|
|
121
|
-
" prompt = hook_data.get('prompt', '')\n"
|
|
122
|
-
"except Exception:\n"
|
|
123
|
-
" prompt = ''\n"
|
|
124
|
-
"task = (prompt[:200].strip() or 'auto') if prompt else 'auto'\n"
|
|
125
|
-
# Background repack when repo changed since last pack.
|
|
126
|
-
"if current_hash != reminded_hash:\n"
|
|
127
|
-
" subprocess.Popen(['agentpack', 'pack', '--task', task, '--mode', 'balanced'],\n"
|
|
128
|
-
" stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n"
|
|
129
|
-
" sentinel.write_text(current_hash or '1')\n"
|
|
130
|
-
" msg = 'AgentPack: repo changed — repacking index. Call agentpack_pack_context(task=\\\"...\\\") for fresh context.'\n"
|
|
131
|
-
"else:\n"
|
|
132
|
-
" msg = 'AgentPack MCP ready. Call agentpack_pack_context(task=\\\"...\\\") before editing files.'\n"
|
|
133
|
-
"print(json.dumps({'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit',\n"
|
|
134
|
-
" 'additionalContext': msg}}))\n"
|
|
135
|
-
"\""
|
|
136
|
-
)
|
|
100
|
+
session_start.append({"hooks": [{"type": "command", "command": session_hook_cmd}]})
|
|
101
|
+
|
|
102
|
+
# UserPromptSubmit: delegate to `agentpack hook` CLI subcommand.
|
|
103
|
+
# - Reads prompt from stdin, uses it as pack task keyword.
|
|
104
|
+
# - With MCP: emits Option-B hint (task + top files list, ~100 tokens).
|
|
105
|
+
# - Without MCP: emits capped fallback (top files, hard cap 3k chars).
|
|
106
|
+
# - Background repacks when root_hash changes (content-addressed, not mtime).
|
|
107
|
+
hook_cmd = "agentpack hook --event UserPromptSubmit"
|
|
137
108
|
user_prompt = hooks.setdefault("UserPromptSubmit", [])
|
|
138
|
-
# Remove stale agentpack hooks (old injection hooks
|
|
109
|
+
# Remove stale agentpack hooks (old injection hooks, old inline MCP reminder).
|
|
139
110
|
def _is_stale_agentpack_hook(cmd: str) -> bool:
|
|
140
111
|
return (
|
|
141
112
|
"context.claude.md" in cmd
|
|
142
113
|
or ".context_injected" in cmd
|
|
143
|
-
or (".mcp_reminded" in cmd and "
|
|
114
|
+
or (".mcp_reminded" in cmd and "python3" in cmd) # old inline python hooks
|
|
144
115
|
)
|
|
145
116
|
for entry in user_prompt:
|
|
146
117
|
entry["hooks"] = [
|
|
@@ -149,14 +120,14 @@ class ClaudeInstaller:
|
|
|
149
120
|
]
|
|
150
121
|
user_prompt[:] = [e for e in user_prompt if e.get("hooks")]
|
|
151
122
|
already_has_prompt_hook = any(
|
|
152
|
-
any(h.get("command", "") ==
|
|
123
|
+
any(h.get("command", "") == hook_cmd for h in entry.get("hooks", []))
|
|
153
124
|
for entry in user_prompt
|
|
154
125
|
)
|
|
155
126
|
if not already_has_prompt_hook:
|
|
156
127
|
user_prompt.append({
|
|
157
128
|
"hooks": [{
|
|
158
129
|
"type": "command",
|
|
159
|
-
"command":
|
|
130
|
+
"command": hook_cmd,
|
|
160
131
|
"timeout": 5,
|
|
161
132
|
"statusMessage": "Checking agentpack index...",
|
|
162
133
|
}]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|