agentpack-cli 0.1.0__tar.gz → 0.1.2__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.0 → agentpack_cli-0.1.2}/PKG-INFO +19 -5
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/README.md +18 -4
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/pyproject.toml +1 -1
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/application/pack_service.py +6 -3
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/session.py +3 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/watch.py +9 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/git.py +20 -7
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/ignore.py +3 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/scanner.py +27 -3
- agentpack_cli-0.1.2/src/agentpack/core/token_estimator.py +40 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/claude.py +11 -1
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/integrations/global_install.py +18 -8
- agentpack_cli-0.1.0/src/agentpack/core/token_estimator.py +0 -26
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/.gitignore +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/LICENSE +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/base.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/claude.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/codex.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/cursor.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/generic.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/windsurf.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/dependency_graph.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/go_imports.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/java_imports.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/js_ts_imports.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/python_imports.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/ranking.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/rust_imports.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/symbols.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/tests.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/application/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/cli.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/_shared.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/benchmark.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/claude_cmd.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/diff.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/doctor.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/explain.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/init.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/install.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/monitor.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/pack.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/scan.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/stats.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/status.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/summarize.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/bootstrap.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/cache.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/config.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/context_pack.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/diff.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/git_hooks.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/global_install.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/merkle.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/models.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/redactor.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/snapshot.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/vscode_tasks.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/data/agentpack.md +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/codex.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/cursor.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/windsurf.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/integrations/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/integrations/git_hooks.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/integrations/vscode_tasks.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/renderers/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/renderers/compact.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/renderers/markdown.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/renderers/receipts.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/session/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/session/state.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/summaries/__init__.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/summaries/base.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/summaries/llm.py +0 -0
- {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/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.2
|
|
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
|
|
@@ -38,7 +38,12 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
|
|
39
39
|
# AgentPack
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
[](https://pypi.org/project/agentpack-cli/)
|
|
42
|
+
[](https://pypi.org/project/agentpack-cli/)
|
|
43
|
+
[](https://opensource.org/licenses/MIT)
|
|
44
|
+
[](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
|
|
45
|
+
|
|
46
|
+
> **Status: alpha (v0.1.2).** 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.
|
|
42
47
|
>
|
|
43
48
|
> **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.
|
|
44
49
|
|
|
@@ -389,10 +394,19 @@ Token counts use tiktoken `cl100k_base` — a close approximation to Claude's ac
|
|
|
389
394
|
|
|
390
395
|
## CI/CD: pack per PR
|
|
391
396
|
|
|
392
|
-
|
|
397
|
+
### agentpack's own CI
|
|
398
|
+
|
|
399
|
+
agentpack uses two workflows:
|
|
400
|
+
|
|
401
|
+
- **`ci.yml`** — runs tests on Python 3.10–3.13 on every push and pull request to `main`
|
|
402
|
+
- **`release.yml`** — runs tests then publishes to PyPI on every `v*` tag push (uses PyPI trusted publishing)
|
|
403
|
+
|
|
404
|
+
### Add context packing to your repo
|
|
405
|
+
|
|
406
|
+
Add to `.github/workflows/agentpack-context.yml`:
|
|
393
407
|
|
|
394
408
|
```yaml
|
|
395
|
-
name: AgentPack context
|
|
409
|
+
name: AgentPack context pack
|
|
396
410
|
|
|
397
411
|
on:
|
|
398
412
|
pull_request:
|
|
@@ -1217,7 +1231,7 @@ agentpack pack --agent claude --task "refactor database connection pooling" --mo
|
|
|
1217
1231
|
|
|
1218
1232
|
### CI: automated context on every PR
|
|
1219
1233
|
|
|
1220
|
-
Add to `.github/workflows/agentpack.yml` — see the full example in [CI/CD: pack per PR](#cicd-pack-per-pr). Reviewers and CI bots get focused context without cloning the repo.
|
|
1234
|
+
Add to `.github/workflows/agentpack-context.yml` — see the full example in [CI/CD: pack per PR](#cicd-pack-per-pr). Reviewers and CI bots get focused context without cloning the repo.
|
|
1221
1235
|
|
|
1222
1236
|
---
|
|
1223
1237
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# AgentPack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://pypi.org/project/agentpack-cli/)
|
|
4
|
+
[](https://pypi.org/project/agentpack-cli/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml)
|
|
7
|
+
|
|
8
|
+
> **Status: alpha (v0.1.2).** 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.
|
|
4
9
|
>
|
|
5
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.
|
|
6
11
|
|
|
@@ -351,10 +356,19 @@ Token counts use tiktoken `cl100k_base` — a close approximation to Claude's ac
|
|
|
351
356
|
|
|
352
357
|
## CI/CD: pack per PR
|
|
353
358
|
|
|
354
|
-
|
|
359
|
+
### agentpack's own CI
|
|
360
|
+
|
|
361
|
+
agentpack uses two workflows:
|
|
362
|
+
|
|
363
|
+
- **`ci.yml`** — runs tests on Python 3.10–3.13 on every push and pull request to `main`
|
|
364
|
+
- **`release.yml`** — runs tests then publishes to PyPI on every `v*` tag push (uses PyPI trusted publishing)
|
|
365
|
+
|
|
366
|
+
### Add context packing to your repo
|
|
367
|
+
|
|
368
|
+
Add to `.github/workflows/agentpack-context.yml`:
|
|
355
369
|
|
|
356
370
|
```yaml
|
|
357
|
-
name: AgentPack context
|
|
371
|
+
name: AgentPack context pack
|
|
358
372
|
|
|
359
373
|
on:
|
|
360
374
|
pull_request:
|
|
@@ -1179,7 +1193,7 @@ agentpack pack --agent claude --task "refactor database connection pooling" --mo
|
|
|
1179
1193
|
|
|
1180
1194
|
### CI: automated context on every PR
|
|
1181
1195
|
|
|
1182
|
-
Add to `.github/workflows/agentpack.yml` — see the full example in [CI/CD: pack per PR](#cicd-pack-per-pr). Reviewers and CI bots get focused context without cloning the repo.
|
|
1196
|
+
Add to `.github/workflows/agentpack-context.yml` — see the full example in [CI/CD: pack per PR](#cicd-pack-per-pr). Reviewers and CI bots get focused context without cloning the repo.
|
|
1183
1197
|
|
|
1184
1198
|
---
|
|
1185
1199
|
|
|
@@ -90,9 +90,11 @@ class ChangeDetector:
|
|
|
90
90
|
packable: list[FileInfo],
|
|
91
91
|
root: Path,
|
|
92
92
|
since: str | None,
|
|
93
|
+
previous_snap: dict | None = None,
|
|
93
94
|
) -> ChangeSet:
|
|
94
95
|
current_snap = build_snapshot(packable)
|
|
95
|
-
previous_snap
|
|
96
|
+
if previous_snap is None:
|
|
97
|
+
previous_snap = load_snapshot(root)
|
|
96
98
|
snap_diff = diff_snapshots(previous_snap, current_snap)
|
|
97
99
|
changed_from_snap: set[str] = set(snap_diff.added + snap_diff.modified)
|
|
98
100
|
|
|
@@ -160,7 +162,8 @@ class PackPlanner:
|
|
|
160
162
|
phase_times: dict[str, float] = {}
|
|
161
163
|
|
|
162
164
|
t0 = time.perf_counter()
|
|
163
|
-
|
|
165
|
+
previous_snap = load_snapshot(root)
|
|
166
|
+
scan_result = scan(root, ignore_spec, cfg.context.max_file_tokens, previous_snapshot=previous_snap)
|
|
164
167
|
phase_times["scan"] = time.perf_counter() - t0
|
|
165
168
|
|
|
166
169
|
packable = scan_result.packable
|
|
@@ -175,7 +178,7 @@ class PackPlanner:
|
|
|
175
178
|
phase_times["deps"] = time.perf_counter() - t0
|
|
176
179
|
|
|
177
180
|
t0 = time.perf_counter()
|
|
178
|
-
changes = ChangeDetector().detect(packable, root, request.since)
|
|
181
|
+
changes = ChangeDetector().detect(packable, root, request.since, previous_snap=previous_snap)
|
|
179
182
|
phase_times["changes"] = time.perf_counter() - t0
|
|
180
183
|
|
|
181
184
|
t0 = time.perf_counter()
|
|
@@ -25,9 +25,12 @@ def register(app: typer.Typer) -> None:
|
|
|
25
25
|
mode: str = typer.Option("balanced", "--mode", help="Pack mode (minimal|balanced|deep)."),
|
|
26
26
|
task: str = typer.Option("", "--task", help="Initial task description."),
|
|
27
27
|
budget: int = typer.Option(0, "--budget", help="Token budget (0 = config default)."),
|
|
28
|
+
silent: bool = typer.Option(False, "--silent", help="Suppress all output (for use in hooks/scripts)."),
|
|
28
29
|
) -> None:
|
|
29
30
|
"""Start a session: create state files and generate initial context."""
|
|
30
31
|
root = _root()
|
|
32
|
+
if silent:
|
|
33
|
+
console.quiet = True
|
|
31
34
|
state = create_session(root, agent=agent, mode=mode)
|
|
32
35
|
|
|
33
36
|
if task:
|
|
@@ -117,6 +117,11 @@ def _watch_with_watchdog(
|
|
|
117
117
|
try:
|
|
118
118
|
while True:
|
|
119
119
|
time.sleep(0.5)
|
|
120
|
+
current_state = load_session(root)
|
|
121
|
+
if current_state is not None and not current_state.active:
|
|
122
|
+
console.print("\n[dim]Session stopped — watch exiting.[/]")
|
|
123
|
+
observer.stop()
|
|
124
|
+
break
|
|
120
125
|
if _pending[0]:
|
|
121
126
|
now = time.monotonic()
|
|
122
127
|
if now - _last_refresh[0] >= debounce:
|
|
@@ -164,6 +169,10 @@ def _watch_polling(
|
|
|
164
169
|
try:
|
|
165
170
|
while True:
|
|
166
171
|
time.sleep(_POLL_INTERVAL)
|
|
172
|
+
current_state = load_session(root)
|
|
173
|
+
if current_state is not None and not current_state.active:
|
|
174
|
+
console.print("\n[dim]Session stopped — watch exiting.[/]")
|
|
175
|
+
break
|
|
167
176
|
curr = _collect_mtimes()
|
|
168
177
|
changed = {p for p, m in curr.items() if prev.get(p) != m}
|
|
169
178
|
changed |= set(prev) - set(curr)
|
|
@@ -77,7 +77,8 @@ def changed_files_since(root: Path, ref: str) -> set[str]:
|
|
|
77
77
|
def infer_task_from_git(root: Path) -> str:
|
|
78
78
|
"""Infer a task description from branch name, changed files, and recent commits.
|
|
79
79
|
|
|
80
|
-
Priority: branch name
|
|
80
|
+
Priority: branch name + changed files → branch name → changed files →
|
|
81
|
+
recent commit messages (up to 3) → recently modified files → fallback.
|
|
81
82
|
"""
|
|
82
83
|
branch: str | None = None
|
|
83
84
|
branch_out = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"], root)
|
|
@@ -91,9 +92,9 @@ def infer_task_from_git(root: Path) -> str:
|
|
|
91
92
|
changed = changed_files(root)
|
|
92
93
|
file_topic = _topic_from_paths(changed) if changed else None
|
|
93
94
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
log_out = _run(["git", "log", "--oneline", "-
|
|
95
|
+
# Collect recent non-merge commit messages (up to 3) for richer fallback
|
|
96
|
+
commit_msgs: list[str] = []
|
|
97
|
+
log_out = _run(["git", "log", "--oneline", "-10"], root)
|
|
97
98
|
if log_out:
|
|
98
99
|
for line in log_out.splitlines():
|
|
99
100
|
line = line.strip()
|
|
@@ -101,17 +102,29 @@ def infer_task_from_git(root: Path) -> str:
|
|
|
101
102
|
continue
|
|
102
103
|
msg = line.split(" ", 1)[1] if " " in line else line
|
|
103
104
|
if not msg.lower().startswith("merge "):
|
|
104
|
-
|
|
105
|
+
commit_msgs.append(msg)
|
|
106
|
+
if len(commit_msgs) == 3:
|
|
105
107
|
break
|
|
106
108
|
|
|
109
|
+
# When branch is clean (no changed files), fall back to recently touched files
|
|
110
|
+
# so keyword scoring has something to work with beyond the branch name alone.
|
|
111
|
+
if not file_topic and not branch:
|
|
112
|
+
recent = recently_modified_files(root, n=10)
|
|
113
|
+
file_topic = _topic_from_paths(set(recent)) if recent else None
|
|
114
|
+
|
|
107
115
|
if branch and file_topic:
|
|
108
116
|
return f"{branch}: {file_topic}"
|
|
117
|
+
if branch and commit_msgs:
|
|
118
|
+
# Augment bare branch name with latest commit context
|
|
119
|
+
return f"{branch}: {commit_msgs[0]}"
|
|
109
120
|
if branch:
|
|
110
121
|
return branch
|
|
122
|
+
if file_topic and commit_msgs:
|
|
123
|
+
return f"{file_topic}: {commit_msgs[0]}"
|
|
111
124
|
if file_topic:
|
|
112
125
|
return file_topic
|
|
113
|
-
if
|
|
114
|
-
return
|
|
126
|
+
if commit_msgs:
|
|
127
|
+
return "; ".join(commit_msgs[:2])
|
|
115
128
|
return "general development"
|
|
116
129
|
|
|
117
130
|
|
|
@@ -7,7 +7,7 @@ import pathspec
|
|
|
7
7
|
|
|
8
8
|
from agentpack.core.ignore import load_spec, is_ignored
|
|
9
9
|
from agentpack.core.models import FileInfo, ScanResult
|
|
10
|
-
from agentpack.core.token_estimator import estimate_tokens
|
|
10
|
+
from agentpack.core.token_estimator import estimate_tokens, estimate_tokens_bytes
|
|
11
11
|
|
|
12
12
|
BINARY_EXTENSIONS = {
|
|
13
13
|
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg",
|
|
@@ -54,7 +54,7 @@ LANGUAGE_MAP: dict[str, str] = {
|
|
|
54
54
|
".xml": "xml",
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
ALWAYS_SKIP = {".git", ".agentpack"}
|
|
57
|
+
ALWAYS_SKIP = {".git", ".agentpack", ".claude"}
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def file_hash(path: Path) -> str:
|
|
@@ -79,11 +79,14 @@ def scan(
|
|
|
79
79
|
root: Path,
|
|
80
80
|
ignore_spec: pathspec.PathSpec,
|
|
81
81
|
max_file_tokens: int = 4000,
|
|
82
|
+
previous_snapshot: dict | None = None,
|
|
82
83
|
) -> ScanResult:
|
|
83
84
|
packable: list[FileInfo] = []
|
|
84
85
|
ignored: list[FileInfo] = []
|
|
85
86
|
binary: list[FileInfo] = []
|
|
86
87
|
|
|
88
|
+
prev_files: dict[str, dict] = (previous_snapshot or {}).get("files", {})
|
|
89
|
+
|
|
87
90
|
for abs_path in root.rglob("*"):
|
|
88
91
|
if not abs_path.is_file():
|
|
89
92
|
continue
|
|
@@ -125,6 +128,27 @@ def scan(
|
|
|
125
128
|
|
|
126
129
|
size = abs_path.stat().st_size
|
|
127
130
|
lang = LANGUAGE_MAP.get(abs_path.suffix.lower())
|
|
131
|
+
fhash = file_hash(abs_path)
|
|
132
|
+
|
|
133
|
+
# Unchanged file: reuse cached token count, skip content read.
|
|
134
|
+
# Content is loaded lazily by context_pack.select_files() when needed.
|
|
135
|
+
prev = prev_files.get(rel_str)
|
|
136
|
+
if prev and prev.get("hash") == fhash:
|
|
137
|
+
cached_tokens = prev.get("estimated_tokens", estimate_tokens_bytes(size))
|
|
138
|
+
too_large = cached_tokens > max_file_tokens
|
|
139
|
+
packable.append(
|
|
140
|
+
FileInfo(
|
|
141
|
+
path=rel_str,
|
|
142
|
+
abs_path=abs_path,
|
|
143
|
+
language=lang,
|
|
144
|
+
size_bytes=size,
|
|
145
|
+
estimated_tokens=cached_tokens,
|
|
146
|
+
hash=fhash,
|
|
147
|
+
too_large=too_large,
|
|
148
|
+
content=None,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
continue
|
|
128
152
|
|
|
129
153
|
try:
|
|
130
154
|
text = abs_path.read_text(errors="replace")
|
|
@@ -141,7 +165,7 @@ def scan(
|
|
|
141
165
|
language=lang,
|
|
142
166
|
size_bytes=size,
|
|
143
167
|
estimated_tokens=tokens,
|
|
144
|
-
hash=
|
|
168
|
+
hash=fhash,
|
|
145
169
|
too_large=too_large,
|
|
146
170
|
content=text,
|
|
147
171
|
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
_encoder = None
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_encoder():
|
|
9
|
+
global _encoder
|
|
10
|
+
if _encoder is None:
|
|
11
|
+
try:
|
|
12
|
+
import tiktoken
|
|
13
|
+
# Only load tiktoken if its vocab cache already exists — avoids
|
|
14
|
+
# a blocking network download when running inside git hooks.
|
|
15
|
+
cache_dir = os.environ.get(
|
|
16
|
+
"TIKTOKEN_CACHE_DIR",
|
|
17
|
+
os.path.join(os.path.expanduser("~"), ".cache", "huggingface", "hub"),
|
|
18
|
+
)
|
|
19
|
+
tiktoken_cache = os.path.join(os.path.expanduser("~"), ".cache", "tiktoken")
|
|
20
|
+
cache_warm = os.path.isdir(tiktoken_cache) and any(
|
|
21
|
+
True for _ in os.scandir(tiktoken_cache)
|
|
22
|
+
) if os.path.isdir(tiktoken_cache) else False
|
|
23
|
+
if cache_warm or os.environ.get("AGENTPACK_FORCE_TIKTOKEN"):
|
|
24
|
+
_encoder = tiktoken.get_encoding("cl100k_base")
|
|
25
|
+
else:
|
|
26
|
+
_encoder = False
|
|
27
|
+
except ImportError:
|
|
28
|
+
_encoder = False
|
|
29
|
+
return _encoder
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def estimate_tokens(text: str) -> int:
|
|
33
|
+
enc = _get_encoder()
|
|
34
|
+
if enc:
|
|
35
|
+
return max(1, len(enc.encode(text, disallowed_special=())))
|
|
36
|
+
return max(1, len(text) // 4)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def estimate_tokens_bytes(size_bytes: int) -> int:
|
|
40
|
+
return max(1, size_bytes // 4)
|
|
@@ -124,7 +124,17 @@ class ClaudeInstaller:
|
|
|
124
124
|
" stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n"
|
|
125
125
|
"if not ctx.exists(): sys.exit(0)\n"
|
|
126
126
|
"content = ctx.read_text()\n"
|
|
127
|
-
"if len(content) > 60000
|
|
127
|
+
"if len(content) > 60000:\n"
|
|
128
|
+
" lines = content.splitlines(keepends=True)\n"
|
|
129
|
+
" kept, total, omit_start = [], 0, None\n"
|
|
130
|
+
" for i, line in enumerate(lines):\n"
|
|
131
|
+
" if total + len(line) > 60000:\n"
|
|
132
|
+
" omit_start = i\n"
|
|
133
|
+
" break\n"
|
|
134
|
+
" kept.append(line)\n"
|
|
135
|
+
" total += len(line)\n"
|
|
136
|
+
" omitted = len(lines) - (omit_start or len(lines))\n"
|
|
137
|
+
" content = ''.join(kept) + f'\\n\\n... [truncated: {omitted} lines omitted]'\n"
|
|
128
138
|
"sentinel.write_text(current_hash or '1')\n"
|
|
129
139
|
"print(json.dumps({'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit',\n"
|
|
130
140
|
" 'additionalContext': '[agentpack: context injected]\\n\\n' + content}}))\n"
|
|
@@ -12,26 +12,28 @@ from pathlib import Path
|
|
|
12
12
|
_GIT_TEMPLATE_DIR = Path.home() / ".git-templates"
|
|
13
13
|
_AGENTPACK_MARKER = "# agentpack:global"
|
|
14
14
|
|
|
15
|
+
_REPACK_CMD = """\
|
|
16
|
+
( agentpack pack --task auto --mode balanced >/dev/null 2>&1 &
|
|
17
|
+
_ap_pid=$! ; ( sleep 30 && kill $_ap_pid 2>/dev/null ) &
|
|
18
|
+
)"""
|
|
19
|
+
|
|
15
20
|
_POST_CHECKOUT_SCRIPT = """\
|
|
16
21
|
#!/bin/sh
|
|
17
22
|
# agentpack:global
|
|
18
23
|
# Repack only if this repo has already been opted in to agentpack.
|
|
19
|
-
[ -f .agentpack/config.toml ] &&
|
|
20
|
-
"""
|
|
24
|
+
[ -f .agentpack/config.toml ] && """ + _REPACK_CMD.strip() + "\n"
|
|
21
25
|
|
|
22
26
|
_POST_COMMIT_SCRIPT = """\
|
|
23
27
|
#!/bin/sh
|
|
24
28
|
# agentpack:global
|
|
25
29
|
# Repack only if this repo has already been opted in to agentpack.
|
|
26
|
-
[ -f .agentpack/config.toml ] &&
|
|
27
|
-
"""
|
|
30
|
+
[ -f .agentpack/config.toml ] && """ + _REPACK_CMD.strip() + "\n"
|
|
28
31
|
|
|
29
32
|
_POST_MERGE_SCRIPT = """\
|
|
30
33
|
#!/bin/sh
|
|
31
34
|
# agentpack:global
|
|
32
35
|
# Repack only if this repo has already been opted in to agentpack.
|
|
33
|
-
[ -f .agentpack/config.toml ] &&
|
|
34
|
-
"""
|
|
36
|
+
[ -f .agentpack/config.toml ] && """ + _REPACK_CMD.strip() + "\n"
|
|
35
37
|
|
|
36
38
|
_HOOK_SCRIPTS = {
|
|
37
39
|
"post-checkout": _POST_CHECKOUT_SCRIPT,
|
|
@@ -142,7 +144,11 @@ _agentpack_chpwd() {
|
|
|
142
144
|
# Only act on repos explicitly opted in (have .agentpack/config.toml).
|
|
143
145
|
# Does NOT auto-init unknown repos — that's an explicit 'agentpack init' decision.
|
|
144
146
|
if [ -f .agentpack/config.toml ]; then
|
|
145
|
-
|
|
147
|
+
if [ ! -f .agentpack/context.md ] && [ ! -f .agentpack/session.json ]; then
|
|
148
|
+
agentpack session start --silent >/dev/null 2>&1 &
|
|
149
|
+
else
|
|
150
|
+
agentpack status >/dev/null 2>&1 || agentpack pack --task auto --mode balanced >/dev/null 2>&1 &
|
|
151
|
+
fi
|
|
146
152
|
fi
|
|
147
153
|
}
|
|
148
154
|
autoload -Uz add-zsh-hook
|
|
@@ -154,7 +160,11 @@ _BASH_HOOK = """\
|
|
|
154
160
|
_agentpack_chpwd() {
|
|
155
161
|
# Only act on repos explicitly opted in (have .agentpack/config.toml).
|
|
156
162
|
if [ -f .agentpack/config.toml ]; then
|
|
157
|
-
|
|
163
|
+
if [ ! -f .agentpack/context.md ] && [ ! -f .agentpack/session.json ]; then
|
|
164
|
+
agentpack session start --silent >/dev/null 2>&1 &
|
|
165
|
+
else
|
|
166
|
+
agentpack status >/dev/null 2>&1 || agentpack pack --task auto --mode balanced >/dev/null 2>&1 &
|
|
167
|
+
fi
|
|
158
168
|
fi
|
|
159
169
|
}
|
|
160
170
|
if [[ "$PROMPT_COMMAND" != *"_agentpack_chpwd"* ]]; then
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
_encoder = None
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def _get_encoder():
|
|
7
|
-
global _encoder
|
|
8
|
-
if _encoder is None:
|
|
9
|
-
try:
|
|
10
|
-
import tiktoken
|
|
11
|
-
_encoder = tiktoken.get_encoding("cl100k_base")
|
|
12
|
-
except ImportError:
|
|
13
|
-
_encoder = False
|
|
14
|
-
return _encoder
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def estimate_tokens(text: str) -> int:
|
|
18
|
-
enc = _get_encoder()
|
|
19
|
-
if enc:
|
|
20
|
-
return max(1, len(enc.encode(text, disallowed_special=())))
|
|
21
|
-
return max(1, len(text) // 4)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def estimate_tokens_bytes(size_bytes: int) -> int:
|
|
25
|
-
# byte-level fallback when text is unavailable
|
|
26
|
-
return max(1, size_bytes // 4)
|
|
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
|