agentpack-cli 0.1.16__tar.gz → 0.1.20__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.20}/PKG-INFO +20 -4
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/README.md +19 -3
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/pyproject.toml +1 -1
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/__init__.py +1 -1
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/application/pack_service.py +6 -1
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/cli.py +2 -2
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/doctor.py +30 -2
- agentpack_cli-0.1.20/src/agentpack/commands/hook_cmd.py +248 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/pack.py +13 -2
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/git.py +64 -27
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/installers/claude.py +22 -51
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/.gitignore +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/LICENSE +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/adapters/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/adapters/antigravity.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/adapters/base.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/adapters/claude.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/adapters/codex.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/adapters/cursor.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/adapters/detect.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/adapters/generic.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/adapters/windsurf.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/dependency_graph.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/go_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/java_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/js_ts_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/python_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/ranking.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/rust_imports.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/symbols.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/analysis/tests.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/application/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/_shared.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/benchmark.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/claude_cmd.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/diff.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/explain.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/init.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/install.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/mcp_cmd.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/monitor.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/scan.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/stats.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/status.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/summarize.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/commands/watch.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/bootstrap.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/cache.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/config.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/context_pack.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/diff.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/git_hooks.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/global_install.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/ignore.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/merkle.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/models.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/redactor.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/scanner.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/snapshot.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/token_estimator.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/core/vscode_tasks.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/data/agentpack.md +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/installers/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/installers/antigravity.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/installers/codex.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/installers/cursor.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/installers/windsurf.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/integrations/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/integrations/git_hooks.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/integrations/global_install.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/integrations/vscode_tasks.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/mcp_server.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/renderers/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/renderers/compact.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/renderers/markdown.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/renderers/receipts.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/session/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/session/state.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/summaries/__init__.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/src/agentpack/summaries/base.py +0 -0
- {agentpack_cli-0.1.16 → agentpack_cli-0.1.20}/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.20
|
|
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.20).** 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
|
|
|
@@ -1298,7 +1298,23 @@ Skip writing a task description — agentpack infers it from your branch name, c
|
|
|
1298
1298
|
agentpack pack --task auto
|
|
1299
1299
|
```
|
|
1300
1300
|
|
|
1301
|
-
Priority order
|
|
1301
|
+
Priority order (strongest → weakest):
|
|
1302
|
+
|
|
1303
|
+
| Source | Example output |
|
|
1304
|
+
|--------|---------------|
|
|
1305
|
+
| `task.md` (explicit) | `"migrate DB schema to multi-tenant"` |
|
|
1306
|
+
| branch + staged files | `"feat add-rate-limiting: payments, throttle"` |
|
|
1307
|
+
| staged files only | `"payments, throttle"` |
|
|
1308
|
+
| branch + unstaged | `"feat add-rate-limiting: session, token"` |
|
|
1309
|
+
| branch + latest commit | `"feat add-rate-limiting: fix token expiry check"` |
|
|
1310
|
+
| branch name alone | `"feat add-rate-limiting"` |
|
|
1311
|
+
| unstaged files | `"session, token"` |
|
|
1312
|
+
| recent commit messages | `"fix token expiry check; add pagination"` |
|
|
1313
|
+
| recently modified files | `"session, payments"` (noisy — last resort) |
|
|
1314
|
+
|
|
1315
|
+
The heuristic that fired is logged: `Auto task (branch+staged): feat add-rate-limiting: payments`.
|
|
1316
|
+
|
|
1317
|
+
The more descriptive your branch names (`feat/add-rate-limiting` beats `dev`) and the more you stage before running, the more accurate the inference.
|
|
1302
1318
|
|
|
1303
1319
|
### Concept synonym expansion
|
|
1304
1320
|
|
|
@@ -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.20).** 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
|
|
|
@@ -1259,7 +1259,23 @@ Skip writing a task description — agentpack infers it from your branch name, c
|
|
|
1259
1259
|
agentpack pack --task auto
|
|
1260
1260
|
```
|
|
1261
1261
|
|
|
1262
|
-
Priority order
|
|
1262
|
+
Priority order (strongest → weakest):
|
|
1263
|
+
|
|
1264
|
+
| Source | Example output |
|
|
1265
|
+
|--------|---------------|
|
|
1266
|
+
| `task.md` (explicit) | `"migrate DB schema to multi-tenant"` |
|
|
1267
|
+
| branch + staged files | `"feat add-rate-limiting: payments, throttle"` |
|
|
1268
|
+
| staged files only | `"payments, throttle"` |
|
|
1269
|
+
| branch + unstaged | `"feat add-rate-limiting: session, token"` |
|
|
1270
|
+
| branch + latest commit | `"feat add-rate-limiting: fix token expiry check"` |
|
|
1271
|
+
| branch name alone | `"feat add-rate-limiting"` |
|
|
1272
|
+
| unstaged files | `"session, token"` |
|
|
1273
|
+
| recent commit messages | `"fix token expiry check; add pagination"` |
|
|
1274
|
+
| recently modified files | `"session, payments"` (noisy — last resort) |
|
|
1275
|
+
|
|
1276
|
+
The heuristic that fired is logged: `Auto task (branch+staged): feat add-rate-limiting: payments`.
|
|
1277
|
+
|
|
1278
|
+
The more descriptive your branch names (`feat/add-rate-limiting` beats `dev`) and the more you stage before running, the more accurate the inference.
|
|
1263
1279
|
|
|
1264
1280
|
### Concept synonym expansion
|
|
1265
1281
|
|
|
@@ -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,248 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from agentpack.commands._shared import _root
|
|
12
|
+
from agentpack.core import git as _git
|
|
13
|
+
|
|
14
|
+
_TASK_FILE = ".agentpack/task.md"
|
|
15
|
+
_TASK_FILE_DEFAULT_MARKER = "Write or update the current coding task here."
|
|
16
|
+
_CODING_PROMPT_RE = re.compile(
|
|
17
|
+
r"(?:fix|add|refactor|impl|implement|update|write|debug|test|build|migrate|remove|delete|rename|optimize)\b",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register(app: typer.Typer) -> None:
|
|
23
|
+
@app.command(name="hook")
|
|
24
|
+
def hook(
|
|
25
|
+
event: str = typer.Option("UserPromptSubmit", "--event", help="Hook event name."),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Run as a Claude Code hook. Reads stdin (JSON), emits additionalContext."""
|
|
28
|
+
root = _root()
|
|
29
|
+
if event == "UserPromptSubmit":
|
|
30
|
+
_run_user_prompt_submit(root)
|
|
31
|
+
elif event == "SessionStart":
|
|
32
|
+
_run_session_start(root)
|
|
33
|
+
else:
|
|
34
|
+
sys.exit(0)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Public helpers (tested directly)
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
def _mcp_installed(root: Path) -> bool:
|
|
42
|
+
local_mcp = root / ".mcp.json"
|
|
43
|
+
if local_mcp.exists():
|
|
44
|
+
try:
|
|
45
|
+
cfg = json.loads(local_mcp.read_text())
|
|
46
|
+
if "agentpack" in cfg.get("mcpServers", {}):
|
|
47
|
+
return True
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
global_settings = Path.home() / ".claude" / "settings.json"
|
|
51
|
+
if global_settings.exists():
|
|
52
|
+
try:
|
|
53
|
+
cfg = json.loads(global_settings.read_text())
|
|
54
|
+
if "agentpack" in cfg.get("mcpServers", {}):
|
|
55
|
+
return True
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_task_md(root: Path) -> str:
|
|
62
|
+
"""Return task.md content if user has written a real task (not the default placeholder)."""
|
|
63
|
+
task_path = root / _TASK_FILE
|
|
64
|
+
if not task_path.exists():
|
|
65
|
+
return ""
|
|
66
|
+
try:
|
|
67
|
+
content = task_path.read_text(encoding="utf-8").strip()
|
|
68
|
+
# Strip markdown heading
|
|
69
|
+
lines = [ln for ln in content.splitlines() if not ln.startswith("#")]
|
|
70
|
+
body = "\n".join(lines).strip()
|
|
71
|
+
if not body or _TASK_FILE_DEFAULT_MARKER in body:
|
|
72
|
+
return ""
|
|
73
|
+
return body[:200]
|
|
74
|
+
except Exception:
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _looks_like_coding_prompt(prompt: str) -> bool:
|
|
79
|
+
"""Return True if prompt looks like a coding task (not a slash command or chat)."""
|
|
80
|
+
stripped = prompt.strip()
|
|
81
|
+
if stripped.startswith("/"):
|
|
82
|
+
return False
|
|
83
|
+
return bool(_CODING_PROMPT_RE.search(stripped))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _resolve_task(root: Path, prompt: str) -> str:
|
|
87
|
+
"""Merge task.md + prompt into best task description for repack."""
|
|
88
|
+
task_md = _load_task_md(root)
|
|
89
|
+
if task_md:
|
|
90
|
+
return task_md
|
|
91
|
+
if prompt and _looks_like_coding_prompt(prompt):
|
|
92
|
+
return prompt[:200].strip()
|
|
93
|
+
return "auto"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _load_hints(root: Path, n: int = 5) -> list[dict]:
|
|
97
|
+
"""Return top-n selected_hints (path + why) from last metrics record."""
|
|
98
|
+
metrics_path = root / ".agentpack" / "metrics.jsonl"
|
|
99
|
+
if not metrics_path.exists():
|
|
100
|
+
return []
|
|
101
|
+
try:
|
|
102
|
+
lines = metrics_path.read_text(encoding="utf-8").splitlines()
|
|
103
|
+
for line in reversed(lines):
|
|
104
|
+
line = line.strip()
|
|
105
|
+
if not line:
|
|
106
|
+
continue
|
|
107
|
+
rec = json.loads(line)
|
|
108
|
+
hints = rec.get("selected_hints", [])
|
|
109
|
+
if hints:
|
|
110
|
+
return hints[:n]
|
|
111
|
+
# Fallback: old metrics without hints
|
|
112
|
+
paths = rec.get("selected_paths", [])
|
|
113
|
+
if paths:
|
|
114
|
+
return [{"path": p, "why": ""} for p in paths[:n]]
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _load_top_files(root: Path, n: int = 5) -> list[dict]:
|
|
121
|
+
"""Alias kept for backward compat with tests."""
|
|
122
|
+
return _load_hints(root, n)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _load_pack_task(root: Path) -> str:
|
|
126
|
+
meta_path = root / ".agentpack" / "pack_metadata.json"
|
|
127
|
+
if not meta_path.exists():
|
|
128
|
+
return ""
|
|
129
|
+
try:
|
|
130
|
+
return json.loads(meta_path.read_text()).get("task", "")
|
|
131
|
+
except Exception:
|
|
132
|
+
return ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _infer_live_task(root: Path) -> str:
|
|
136
|
+
"""Live task: git priority chain (no stale metadata). Falls back to 'unknown'."""
|
|
137
|
+
try:
|
|
138
|
+
task, _ = _git.infer_task_with_source(root)
|
|
139
|
+
return task
|
|
140
|
+
except Exception:
|
|
141
|
+
return "unknown"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _current_root_hash(root: Path) -> str | None:
|
|
145
|
+
snap = root / ".agentpack" / "snapshots" / "latest.json"
|
|
146
|
+
if not snap.exists():
|
|
147
|
+
return None
|
|
148
|
+
try:
|
|
149
|
+
return json.loads(snap.read_text()).get("root_hash")
|
|
150
|
+
except Exception:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Event handlers
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def _run_session_start(root: Path) -> None:
|
|
159
|
+
"""Clear sentinels so first prompt gets fresh context."""
|
|
160
|
+
for sentinel in [
|
|
161
|
+
root / ".agentpack" / ".mcp_reminded",
|
|
162
|
+
root / ".agentpack" / ".context_injected",
|
|
163
|
+
]:
|
|
164
|
+
try:
|
|
165
|
+
sentinel.unlink(missing_ok=True)
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
# No output needed — SessionStart hooks don't inject additionalContext
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _run_user_prompt_submit(root: Path) -> None:
|
|
172
|
+
snap_sentinel = root / ".agentpack" / ".mcp_reminded"
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
hook_data = json.loads(sys.stdin.read())
|
|
176
|
+
prompt = hook_data.get("prompt", "")
|
|
177
|
+
except Exception:
|
|
178
|
+
prompt = ""
|
|
179
|
+
|
|
180
|
+
task = _resolve_task(root, prompt)
|
|
181
|
+
|
|
182
|
+
current_hash = _current_root_hash(root)
|
|
183
|
+
reminded_hash = snap_sentinel.read_text().strip() if snap_sentinel.exists() else None
|
|
184
|
+
repo_changed = current_hash != reminded_hash
|
|
185
|
+
|
|
186
|
+
if repo_changed:
|
|
187
|
+
subprocess.Popen(
|
|
188
|
+
["agentpack", "pack", "--task", task, "--mode", "balanced", "--since", "HEAD~1"],
|
|
189
|
+
stdout=subprocess.DEVNULL,
|
|
190
|
+
stderr=subprocess.DEVNULL,
|
|
191
|
+
)
|
|
192
|
+
try:
|
|
193
|
+
snap_sentinel.write_text(current_hash or "1")
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
has_mcp = _mcp_installed(root)
|
|
198
|
+
|
|
199
|
+
if has_mcp:
|
|
200
|
+
hints = _load_hints(root, n=5)
|
|
201
|
+
if hints:
|
|
202
|
+
files_lines = "\n".join(
|
|
203
|
+
f" - {h['path']}" + (f" — {h['why']}" if h.get("why") else "")
|
|
204
|
+
for h in hints
|
|
205
|
+
)
|
|
206
|
+
status_note = "(repacking — call pack_context for fresh results)" if repo_changed else "(index fresh)"
|
|
207
|
+
current_task = _load_task_md(root) or _infer_live_task(root)
|
|
208
|
+
msg = (
|
|
209
|
+
f"AgentPack {status_note}\n"
|
|
210
|
+
f"task: {current_task}\n"
|
|
211
|
+
f"top files:\n{files_lines}\n"
|
|
212
|
+
f"Call agentpack_pack_context(task=\"...\") for full ranked context."
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
msg = (
|
|
216
|
+
"AgentPack active. No pack yet — call agentpack_pack_context(task=\"...\") "
|
|
217
|
+
"to build context for this task."
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
hints = _load_hints(root, n=8)
|
|
221
|
+
current_task = _load_task_md(root) or _infer_live_task(root)
|
|
222
|
+
if hints:
|
|
223
|
+
files_lines = "\n".join(
|
|
224
|
+
f" - {h['path']}" + (f" — {h['why']}" if h.get("why") else "")
|
|
225
|
+
for h in hints
|
|
226
|
+
)
|
|
227
|
+
changed_note = " (repacking in background)" if repo_changed else ""
|
|
228
|
+
msg = (
|
|
229
|
+
f"AgentPack context{changed_note}\n"
|
|
230
|
+
f"task: {current_task}\n"
|
|
231
|
+
f"top files:\n{files_lines}\n\n"
|
|
232
|
+
f"For richer context, install MCP: agentpack install --agent claude"
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
msg = (
|
|
236
|
+
"AgentPack active. Run `agentpack pack --task \"<task>\"` to build context.\n"
|
|
237
|
+
"For auto context, install MCP: agentpack install --agent claude"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if len(msg) > 3000:
|
|
241
|
+
msg = msg[:2970] + "\n... [truncated]"
|
|
242
|
+
|
|
243
|
+
print(json.dumps({
|
|
244
|
+
"hookSpecificOutput": {
|
|
245
|
+
"hookEventName": "UserPromptSubmit",
|
|
246
|
+
"additionalContext": msg,
|
|
247
|
+
}
|
|
248
|
+
}))
|
|
@@ -64,8 +64,19 @@ def _resolve_agent(agent: str) -> str:
|
|
|
64
64
|
def _resolve_task(task: str) -> str:
|
|
65
65
|
if task != "auto":
|
|
66
66
|
return task
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
root = _root()
|
|
68
|
+
# task.md takes priority over all git heuristics
|
|
69
|
+
task_md_path = root / ".agentpack" / "task.md"
|
|
70
|
+
if task_md_path.exists():
|
|
71
|
+
content = task_md_path.read_text(encoding="utf-8").strip()
|
|
72
|
+
lines = [ln for ln in content.splitlines() if ln.strip() and not ln.startswith("#")]
|
|
73
|
+
body = lines[0].strip() if lines else ""
|
|
74
|
+
_PLACEHOLDER = "Write or update the current coding task here."
|
|
75
|
+
if body and _PLACEHOLDER not in body:
|
|
76
|
+
console.print(f"[dim]Auto task (task.md): {body}[/]")
|
|
77
|
+
return body
|
|
78
|
+
inferred, source = git.infer_task_with_source(root)
|
|
79
|
+
console.print(f"[dim]Auto task ({source}): {inferred}[/]")
|
|
69
80
|
return inferred
|
|
70
81
|
|
|
71
82
|
|
|
@@ -94,12 +94,44 @@ def file_churn_counts(root: Path, max_commits: int = 200) -> dict[str, int]:
|
|
|
94
94
|
return counts
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
def staged_files(root: Path) -> set[str]:
|
|
98
|
+
"""Files staged for commit (git index only)."""
|
|
99
|
+
out = _run(["git", "diff", "--cached", "--name-only"], root)
|
|
100
|
+
if not out:
|
|
101
|
+
return set()
|
|
102
|
+
return {line.strip() for line in out.splitlines() if line.strip()}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def infer_task_with_source(root: Path) -> tuple[str, str]:
|
|
106
|
+
"""Infer task description with the heuristic that fired.
|
|
107
|
+
|
|
108
|
+
Priority (strongest → weakest):
|
|
109
|
+
branch+staged staged files present + branch name
|
|
110
|
+
staged staged files, no branch
|
|
111
|
+
branch+unstaged unstaged changes + branch name
|
|
112
|
+
branch+commit branch + latest commit message
|
|
113
|
+
branch branch name alone
|
|
114
|
+
unstaged unstaged changes, no branch
|
|
115
|
+
commits recent commit messages
|
|
116
|
+
recently_modified git log history (noisy — last resort)
|
|
117
|
+
fallback "general development"
|
|
102
118
|
"""
|
|
119
|
+
if not is_git_repo(root):
|
|
120
|
+
return "general development", "fallback"
|
|
121
|
+
|
|
122
|
+
staged = staged_files(root)
|
|
123
|
+
|
|
124
|
+
unstaged_out = _run(["git", "diff", "--name-only"], root)
|
|
125
|
+
unstaged: set[str] = set()
|
|
126
|
+
if unstaged_out:
|
|
127
|
+
for line in unstaged_out.splitlines():
|
|
128
|
+
line = line.strip()
|
|
129
|
+
if line:
|
|
130
|
+
unstaged.add(line)
|
|
131
|
+
|
|
132
|
+
staged_topic = _topic_from_paths(staged) if staged else None
|
|
133
|
+
unstaged_topic = _topic_from_paths(unstaged) if unstaged else None
|
|
134
|
+
|
|
103
135
|
branch: str | None = None
|
|
104
136
|
branch_out = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"], root)
|
|
105
137
|
if branch_out:
|
|
@@ -108,11 +140,6 @@ def infer_task_from_git(root: Path) -> str:
|
|
|
108
140
|
slug = b.split("/", 1)[-1]
|
|
109
141
|
branch = slug.replace("-", " ").replace("_", " ")
|
|
110
142
|
|
|
111
|
-
# Changed files are the strongest signal for *current* work
|
|
112
|
-
changed = changed_files(root)
|
|
113
|
-
file_topic = _topic_from_paths(changed) if changed else None
|
|
114
|
-
|
|
115
|
-
# Collect recent non-merge commit messages (up to 3) for richer fallback
|
|
116
143
|
commit_msgs: list[str] = []
|
|
117
144
|
log_out = _run(["git", "log", "--oneline", "-10"], root)
|
|
118
145
|
if log_out:
|
|
@@ -126,26 +153,36 @@ def infer_task_from_git(root: Path) -> str:
|
|
|
126
153
|
if len(commit_msgs) == 3:
|
|
127
154
|
break
|
|
128
155
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if branch and file_topic:
|
|
136
|
-
return f"{branch}: {file_topic}"
|
|
156
|
+
if branch and staged_topic:
|
|
157
|
+
return f"{branch}: {staged_topic}", "branch+staged"
|
|
158
|
+
if staged_topic:
|
|
159
|
+
return staged_topic, "staged"
|
|
160
|
+
if branch and unstaged_topic:
|
|
161
|
+
return f"{branch}: {unstaged_topic}", "branch+unstaged"
|
|
137
162
|
if branch and commit_msgs:
|
|
138
|
-
|
|
139
|
-
return f"{branch}: {commit_msgs[0]}"
|
|
163
|
+
return f"{branch}: {commit_msgs[0]}", "branch+commit"
|
|
140
164
|
if branch:
|
|
141
|
-
return branch
|
|
142
|
-
if
|
|
143
|
-
return f"{
|
|
144
|
-
if
|
|
145
|
-
return
|
|
165
|
+
return branch, "branch"
|
|
166
|
+
if unstaged_topic and commit_msgs:
|
|
167
|
+
return f"{unstaged_topic}: {commit_msgs[0]}", "unstaged+commit"
|
|
168
|
+
if unstaged_topic:
|
|
169
|
+
return unstaged_topic, "unstaged"
|
|
146
170
|
if commit_msgs:
|
|
147
|
-
return "; ".join(commit_msgs[:2])
|
|
148
|
-
|
|
171
|
+
return "; ".join(commit_msgs[:2]), "commits"
|
|
172
|
+
|
|
173
|
+
# Last resort: historical git log — only fires when no live signal found
|
|
174
|
+
recent = recently_modified_files(root, n=10)
|
|
175
|
+
recent_topic = _topic_from_paths(set(recent)) if recent else None
|
|
176
|
+
if recent_topic:
|
|
177
|
+
return recent_topic, "recently_modified"
|
|
178
|
+
|
|
179
|
+
return "general development", "fallback"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def infer_task_from_git(root: Path) -> str:
|
|
183
|
+
"""Infer a task description from git state. See infer_task_with_source for priority chain."""
|
|
184
|
+
task, _ = infer_task_with_source(root)
|
|
185
|
+
return task
|
|
149
186
|
|
|
150
187
|
|
|
151
188
|
def _topic_from_paths(paths: set[str]) -> str | None:
|
|
@@ -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
|