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.
Files changed (81) hide show
  1. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/PKG-INFO +19 -5
  2. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/README.md +18 -4
  3. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/pyproject.toml +1 -1
  4. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/application/pack_service.py +6 -3
  5. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/session.py +3 -0
  6. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/watch.py +9 -0
  7. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/git.py +20 -7
  8. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/ignore.py +3 -0
  9. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/scanner.py +27 -3
  10. agentpack_cli-0.1.2/src/agentpack/core/token_estimator.py +40 -0
  11. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/claude.py +11 -1
  12. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/integrations/global_install.py +18 -8
  13. agentpack_cli-0.1.0/src/agentpack/core/token_estimator.py +0 -26
  14. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/.gitignore +0 -0
  15. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/LICENSE +0 -0
  16. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/__init__.py +0 -0
  17. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/__init__.py +0 -0
  18. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/base.py +0 -0
  19. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/claude.py +0 -0
  20. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/codex.py +0 -0
  21. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/cursor.py +0 -0
  22. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/generic.py +0 -0
  23. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/adapters/windsurf.py +0 -0
  24. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/__init__.py +0 -0
  25. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/dependency_graph.py +0 -0
  26. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/go_imports.py +0 -0
  27. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/java_imports.py +0 -0
  28. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/js_ts_imports.py +0 -0
  29. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/python_imports.py +0 -0
  30. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/ranking.py +0 -0
  31. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/rust_imports.py +0 -0
  32. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/symbols.py +0 -0
  33. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/analysis/tests.py +0 -0
  34. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/application/__init__.py +0 -0
  35. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/cli.py +0 -0
  36. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/__init__.py +0 -0
  37. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/_shared.py +0 -0
  38. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/benchmark.py +0 -0
  39. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/claude_cmd.py +0 -0
  40. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/diff.py +0 -0
  41. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/doctor.py +0 -0
  42. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/explain.py +0 -0
  43. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/init.py +0 -0
  44. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/install.py +0 -0
  45. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/monitor.py +0 -0
  46. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/pack.py +0 -0
  47. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/scan.py +0 -0
  48. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/stats.py +0 -0
  49. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/status.py +0 -0
  50. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/commands/summarize.py +0 -0
  51. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/__init__.py +0 -0
  52. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/bootstrap.py +0 -0
  53. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/cache.py +0 -0
  54. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/config.py +0 -0
  55. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/context_pack.py +0 -0
  56. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/diff.py +0 -0
  57. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/git_hooks.py +0 -0
  58. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/global_install.py +0 -0
  59. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/merkle.py +0 -0
  60. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/models.py +0 -0
  61. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/redactor.py +0 -0
  62. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/snapshot.py +0 -0
  63. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/core/vscode_tasks.py +0 -0
  64. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/data/agentpack.md +0 -0
  65. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/__init__.py +0 -0
  66. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/codex.py +0 -0
  67. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/cursor.py +0 -0
  68. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/installers/windsurf.py +0 -0
  69. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/integrations/__init__.py +0 -0
  70. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/integrations/git_hooks.py +0 -0
  71. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/integrations/vscode_tasks.py +0 -0
  72. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/renderers/__init__.py +0 -0
  73. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/renderers/compact.py +0 -0
  74. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/renderers/markdown.py +0 -0
  75. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/renderers/receipts.py +0 -0
  76. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/session/__init__.py +0 -0
  77. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/session/state.py +0 -0
  78. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/summaries/__init__.py +0 -0
  79. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/summaries/base.py +0 -0
  80. {agentpack_cli-0.1.0 → agentpack_cli-0.1.2}/src/agentpack/summaries/llm.py +0 -0
  81. {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.0
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
- > **Status: alpha (v0.1.0).** 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.
41
+ [![PyPI version](https://img.shields.io/pypi/v/agentpack-cli.svg)](https://pypi.org/project/agentpack-cli/)
42
+ [![Python versions](https://img.shields.io/pypi/pyversions/agentpack-cli.svg)](https://pypi.org/project/agentpack-cli/)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
44
+ [![CI](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml/badge.svg)](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
- Add to `.github/workflows/agentpack.yml`:
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
- > **Status: alpha (v0.1.0).** 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.
3
+ [![PyPI version](https://img.shields.io/pypi/v/agentpack-cli.svg)](https://pypi.org/project/agentpack-cli/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/agentpack-cli.svg)](https://pypi.org/project/agentpack-cli/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![CI](https://github.com/vishal2612200/agentpack/actions/workflows/ci.yml/badge.svg)](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
- Add to `.github/workflows/agentpack.yml`:
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentpack-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Token-aware context packing for AI coding agents — Claude, Cursor, Windsurf, and Codex"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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 = load_snapshot(root)
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
- scan_result = scan(root, ignore_spec, cfg.context.max_file_tokens)
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 (explicit intent)changed file paths (current work) recent commit.
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
- # Fallback: most recent non-merge commit
95
- commit: str | None = None
96
- log_out = _run(["git", "log", "--oneline", "-5"], root)
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
- commit = msg
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 commit:
114
- return commit
126
+ if commit_msgs:
127
+ return "; ".join(commit_msgs[:2])
115
128
  return "general development"
116
129
 
117
130
 
@@ -51,6 +51,9 @@ Gemfile.lock
51
51
  *.csv
52
52
  *.jsonl
53
53
  *.parquet
54
+
55
+ # claude code internals
56
+ .claude/worktrees/
54
57
  """
55
58
 
56
59
 
@@ -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=file_hash(abs_path),
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: content = content[:60000] + '\\n... [truncated]'\n"
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 ] && agentpack pack --task auto --mode balanced >/dev/null 2>&1 &
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 ] && agentpack pack --task auto --mode balanced >/dev/null 2>&1 &
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 ] && agentpack pack --task auto --mode balanced >/dev/null 2>&1 &
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
- agentpack status >/dev/null 2>&1 || agentpack pack --task auto --mode balanced >/dev/null 2>&1 &
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
- agentpack status >/dev/null 2>&1 || agentpack pack --task auto --mode balanced >/dev/null 2>&1 &
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