agentpack-cli 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- agentpack/adapters/__init__.py +0 -0
- agentpack/adapters/base.py +22 -0
- agentpack/adapters/claude.py +32 -0
- agentpack/adapters/codex.py +26 -0
- agentpack/adapters/cursor.py +29 -0
- agentpack/adapters/generic.py +18 -0
- agentpack/adapters/windsurf.py +26 -0
- agentpack/analysis/__init__.py +0 -0
- agentpack/analysis/dependency_graph.py +80 -0
- agentpack/analysis/go_imports.py +32 -0
- agentpack/analysis/java_imports.py +19 -0
- agentpack/analysis/js_ts_imports.py +53 -0
- agentpack/analysis/python_imports.py +45 -0
- agentpack/analysis/ranking.py +400 -0
- agentpack/analysis/rust_imports.py +32 -0
- agentpack/analysis/symbols.py +154 -0
- agentpack/analysis/tests.py +30 -0
- agentpack/application/__init__.py +0 -0
- agentpack/application/pack_service.py +352 -0
- agentpack/cli.py +33 -0
- agentpack/commands/__init__.py +0 -0
- agentpack/commands/_shared.py +13 -0
- agentpack/commands/benchmark.py +302 -0
- agentpack/commands/claude_cmd.py +55 -0
- agentpack/commands/diff.py +46 -0
- agentpack/commands/doctor.py +185 -0
- agentpack/commands/explain.py +238 -0
- agentpack/commands/init.py +79 -0
- agentpack/commands/install.py +252 -0
- agentpack/commands/monitor.py +105 -0
- agentpack/commands/pack.py +188 -0
- agentpack/commands/scan.py +51 -0
- agentpack/commands/session.py +204 -0
- agentpack/commands/stats.py +138 -0
- agentpack/commands/status.py +37 -0
- agentpack/commands/summarize.py +64 -0
- agentpack/commands/watch.py +185 -0
- agentpack/core/__init__.py +0 -0
- agentpack/core/bootstrap.py +46 -0
- agentpack/core/cache.py +41 -0
- agentpack/core/config.py +101 -0
- agentpack/core/context_pack.py +222 -0
- agentpack/core/diff.py +40 -0
- agentpack/core/git.py +145 -0
- agentpack/core/git_hooks.py +8 -0
- agentpack/core/global_install.py +14 -0
- agentpack/core/ignore.py +66 -0
- agentpack/core/merkle.py +8 -0
- agentpack/core/models.py +115 -0
- agentpack/core/redactor.py +99 -0
- agentpack/core/scanner.py +150 -0
- agentpack/core/snapshot.py +60 -0
- agentpack/core/token_estimator.py +26 -0
- agentpack/core/vscode_tasks.py +5 -0
- agentpack/data/agentpack.md +160 -0
- agentpack/installers/__init__.py +0 -0
- agentpack/installers/claude.py +160 -0
- agentpack/installers/codex.py +54 -0
- agentpack/installers/cursor.py +76 -0
- agentpack/installers/windsurf.py +50 -0
- agentpack/integrations/__init__.py +0 -0
- agentpack/integrations/git_hooks.py +109 -0
- agentpack/integrations/global_install.py +221 -0
- agentpack/integrations/vscode_tasks.py +85 -0
- agentpack/renderers/__init__.py +3 -0
- agentpack/renderers/compact.py +75 -0
- agentpack/renderers/markdown.py +144 -0
- agentpack/renderers/receipts.py +10 -0
- agentpack/session/__init__.py +33 -0
- agentpack/session/state.py +105 -0
- agentpack/summaries/__init__.py +0 -0
- agentpack/summaries/base.py +42 -0
- agentpack/summaries/llm.py +100 -0
- agentpack/summaries/offline.py +97 -0
- agentpack_cli-0.1.0.dist-info/METADATA +1391 -0
- agentpack_cli-0.1.0.dist-info/RECORD +80 -0
- agentpack_cli-0.1.0.dist-info/WHEEL +4 -0
- agentpack_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agentpack_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
agentpack/core/models.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ScanResult(BaseModel):
|
|
7
|
+
packable: list["FileInfo"]
|
|
8
|
+
ignored: list["FileInfo"]
|
|
9
|
+
binary: list["FileInfo"]
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def all_files(self) -> list["FileInfo"]:
|
|
13
|
+
return self.packable + self.ignored + self.binary
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileInfo(BaseModel):
|
|
17
|
+
path: str
|
|
18
|
+
abs_path: Path
|
|
19
|
+
language: str | None = None
|
|
20
|
+
size_bytes: int
|
|
21
|
+
estimated_tokens: int
|
|
22
|
+
hash: str | None = None
|
|
23
|
+
ignored: bool = False
|
|
24
|
+
binary: bool = False
|
|
25
|
+
too_large: bool = False
|
|
26
|
+
content: str | None = None # cached at scan time; avoids re-reads in scoring/selection
|
|
27
|
+
|
|
28
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Symbol(BaseModel):
|
|
32
|
+
name: str
|
|
33
|
+
kind: Literal["class", "function", "method", "variable"]
|
|
34
|
+
start_line: int
|
|
35
|
+
end_line: int
|
|
36
|
+
signature: str | None = None
|
|
37
|
+
summary: str | None = None
|
|
38
|
+
body: str | None = None # source text captured at extraction time; no re-read needed
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FileSummary(BaseModel):
|
|
42
|
+
path: str
|
|
43
|
+
hash: str
|
|
44
|
+
language: str | None = None
|
|
45
|
+
provider: str = "offline"
|
|
46
|
+
schema_version: int = 1
|
|
47
|
+
summary: str
|
|
48
|
+
imports: list[str] = []
|
|
49
|
+
symbols: list[Symbol] = []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SelectedFile(BaseModel):
|
|
53
|
+
path: str
|
|
54
|
+
language: str | None = None
|
|
55
|
+
score: float
|
|
56
|
+
include_mode: Literal["full", "symbols", "summary"]
|
|
57
|
+
reasons: list[str]
|
|
58
|
+
content: str | None = None
|
|
59
|
+
summary: str | None = None
|
|
60
|
+
symbols: list[Symbol] = []
|
|
61
|
+
redaction_warnings: list[str] = []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Receipt(BaseModel):
|
|
65
|
+
path: str
|
|
66
|
+
action: Literal["included", "excluded", "summarized"]
|
|
67
|
+
reason: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ContextPack(BaseModel):
|
|
71
|
+
task: str
|
|
72
|
+
agent: str
|
|
73
|
+
mode: Literal["minimal", "balanced", "deep"]
|
|
74
|
+
budget: int
|
|
75
|
+
token_estimate: int
|
|
76
|
+
raw_repo_tokens: int
|
|
77
|
+
after_ignore_tokens: int
|
|
78
|
+
estimated_savings_percent: float
|
|
79
|
+
changed_files: list[str]
|
|
80
|
+
selected_files: list[SelectedFile]
|
|
81
|
+
receipts: list[Receipt]
|
|
82
|
+
redaction_warnings: list[str] = []
|
|
83
|
+
stale: bool = False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DependencyNode(BaseModel):
|
|
87
|
+
path: str
|
|
88
|
+
imports: list[str] = []
|
|
89
|
+
imported_by: list[str] = []
|
|
90
|
+
tests: list[str] = []
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DependencyGraph(BaseModel):
|
|
94
|
+
nodes: dict[str, DependencyNode] = {}
|
|
95
|
+
|
|
96
|
+
def get(self, path: str) -> DependencyNode:
|
|
97
|
+
return self.nodes.get(path, DependencyNode(path=path))
|
|
98
|
+
|
|
99
|
+
def __getitem__(self, path: str) -> DependencyNode:
|
|
100
|
+
return self.nodes[path]
|
|
101
|
+
|
|
102
|
+
def __setitem__(self, path: str, node: DependencyNode) -> None:
|
|
103
|
+
self.nodes[path] = node
|
|
104
|
+
|
|
105
|
+
def __contains__(self, path: object) -> bool:
|
|
106
|
+
return path in self.nodes
|
|
107
|
+
|
|
108
|
+
def __iter__(self): # type: ignore[override]
|
|
109
|
+
return iter(self.nodes)
|
|
110
|
+
|
|
111
|
+
def __len__(self) -> int:
|
|
112
|
+
return len(self.nodes)
|
|
113
|
+
|
|
114
|
+
def items(self): # type: ignore[override]
|
|
115
|
+
return self.nodes.items()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# Placeholder patterns — values matching these are NOT redacted
|
|
6
|
+
_PLACEHOLDER_RE = re.compile(
|
|
7
|
+
r"your[_-]?(?:api[_-]?)?(?:key|token|secret)[_-]?here|"
|
|
8
|
+
r"<[A-Z_]{3,}(?:KEY|TOKEN|SECRET|PASSWORD)[A-Z_]*>|"
|
|
9
|
+
r"xxx+|"
|
|
10
|
+
r"insert[_-]?(?:key|token|secret)[_-]?here|"
|
|
11
|
+
r"changeme|"
|
|
12
|
+
r"example[_-]?(?:key|token|secret)|"
|
|
13
|
+
r"(?:key|token|secret)[_-]?example",
|
|
14
|
+
re.IGNORECASE,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# (pattern, label, value_group): when value_group is set, only that group is redacted.
|
|
18
|
+
_SECRET_PATTERNS: list[tuple[re.Pattern[str], str, int | None]] = [
|
|
19
|
+
(re.compile(
|
|
20
|
+
r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?"
|
|
21
|
+
r"-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"
|
|
22
|
+
), "private-key", None),
|
|
23
|
+
(re.compile(
|
|
24
|
+
r"eyJ[A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{20,}"
|
|
25
|
+
), "jwt", None),
|
|
26
|
+
(re.compile(r"AKIA[0-9A-Z]{16}"), "aws-access-key", None),
|
|
27
|
+
(re.compile(
|
|
28
|
+
r"(?i)(aws_secret(?:_access_key)?\s*[=:\"'\s]+\s*)([A-Za-z0-9+/]{40})"
|
|
29
|
+
), "aws-secret-key", 2),
|
|
30
|
+
(re.compile(r"gh[pousr]_[A-Za-z0-9]{36,}"), "github-token", None),
|
|
31
|
+
# Anthropic before OpenAI to avoid partial match on sk- prefix
|
|
32
|
+
(re.compile(r"sk-ant-[A-Za-z0-9\-]{32,}"), "anthropic-key", None),
|
|
33
|
+
(re.compile(r"sk-[A-Za-z0-9]{32,}"), "openai-key", None),
|
|
34
|
+
# Generic: handles key=value, key='value', key="value", key: value
|
|
35
|
+
(re.compile(
|
|
36
|
+
r"(?i)(?:api[_-]?key|token|secret|password|passwd|auth[_-]?key)"
|
|
37
|
+
r"\s*[=:]\s*[\"']?\s*([A-Za-z0-9+/\-_]{40,})"
|
|
38
|
+
), "api-key", 1),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_placeholder(value: str) -> bool:
|
|
43
|
+
return bool(_PLACEHOLDER_RE.search(value))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _line_of(text: str, start: int) -> int:
|
|
47
|
+
"""Return 1-indexed line number for character offset *start* in *text*."""
|
|
48
|
+
return text.count("\n", 0, start) + 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def redact_secrets(text: str, path: str) -> tuple[str, list[str]]:
|
|
52
|
+
"""Scan *text* for secrets and replace each with ``[REDACTED:<type>]``.
|
|
53
|
+
|
|
54
|
+
Returns ``(redacted_text, warnings)`` where each warning is a
|
|
55
|
+
human-readable string like ``"src/config.py: aws-access-key detected (line 12)"``.
|
|
56
|
+
"""
|
|
57
|
+
warnings: list[str] = []
|
|
58
|
+
# Collect (start, end, replacement_str) tuples; apply in reverse order.
|
|
59
|
+
replacements: list[tuple[int, int, str]] = []
|
|
60
|
+
# Track redacted spans to avoid double-reporting overlapping matches.
|
|
61
|
+
redacted_spans: list[tuple[int, int]] = []
|
|
62
|
+
|
|
63
|
+
def _overlaps(start: int, end: int) -> bool:
|
|
64
|
+
for rs, re_ in redacted_spans:
|
|
65
|
+
if start < re_ and end > rs:
|
|
66
|
+
return True
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
for pattern, label, value_group in _SECRET_PATTERNS:
|
|
70
|
+
for m in pattern.finditer(text):
|
|
71
|
+
if value_group is not None:
|
|
72
|
+
secret_val = m.group(value_group)
|
|
73
|
+
secret_start = m.start(value_group)
|
|
74
|
+
secret_end = m.end(value_group)
|
|
75
|
+
else:
|
|
76
|
+
secret_val = m.group(0)
|
|
77
|
+
secret_start = m.start()
|
|
78
|
+
secret_end = m.end()
|
|
79
|
+
|
|
80
|
+
if _is_placeholder(secret_val):
|
|
81
|
+
continue
|
|
82
|
+
if _overlaps(secret_start, secret_end):
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
line_no = _line_of(text, secret_start)
|
|
86
|
+
replacements.append((secret_start, secret_end, f"[REDACTED:{label}]"))
|
|
87
|
+
redacted_spans.append((secret_start, secret_end))
|
|
88
|
+
warnings.append(f"{path}: {label} detected (line {line_no})")
|
|
89
|
+
|
|
90
|
+
if not replacements:
|
|
91
|
+
return text, warnings
|
|
92
|
+
|
|
93
|
+
# Apply replacements in reverse order to preserve earlier offsets
|
|
94
|
+
replacements.sort(key=lambda r: r[0], reverse=True)
|
|
95
|
+
chars = list(text)
|
|
96
|
+
for start, end, repl in replacements:
|
|
97
|
+
chars[start:end] = list(repl)
|
|
98
|
+
|
|
99
|
+
return "".join(chars), warnings
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pathspec
|
|
7
|
+
|
|
8
|
+
from agentpack.core.ignore import load_spec, is_ignored
|
|
9
|
+
from agentpack.core.models import FileInfo, ScanResult
|
|
10
|
+
from agentpack.core.token_estimator import estimate_tokens
|
|
11
|
+
|
|
12
|
+
BINARY_EXTENSIONS = {
|
|
13
|
+
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg",
|
|
14
|
+
".pdf", ".zip", ".tar", ".gz", ".bz2", ".7z", ".rar",
|
|
15
|
+
".exe", ".dll", ".so", ".dylib", ".bin",
|
|
16
|
+
".mp3", ".mp4", ".wav", ".avi", ".mov",
|
|
17
|
+
".ttf", ".woff", ".woff2", ".eot",
|
|
18
|
+
".pyc", ".pyo", ".class",
|
|
19
|
+
".db", ".sqlite", ".sqlite3",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
LANGUAGE_MAP: dict[str, str] = {
|
|
23
|
+
".py": "python",
|
|
24
|
+
".js": "javascript",
|
|
25
|
+
".jsx": "javascript",
|
|
26
|
+
".ts": "typescript",
|
|
27
|
+
".tsx": "typescript",
|
|
28
|
+
".mjs": "javascript",
|
|
29
|
+
".cjs": "javascript",
|
|
30
|
+
".go": "go",
|
|
31
|
+
".rs": "rust",
|
|
32
|
+
".java": "java",
|
|
33
|
+
".kt": "kotlin",
|
|
34
|
+
".rb": "ruby",
|
|
35
|
+
".php": "php",
|
|
36
|
+
".c": "c",
|
|
37
|
+
".cpp": "cpp",
|
|
38
|
+
".h": "c",
|
|
39
|
+
".hpp": "cpp",
|
|
40
|
+
".cs": "csharp",
|
|
41
|
+
".html": "html",
|
|
42
|
+
".css": "css",
|
|
43
|
+
".scss": "scss",
|
|
44
|
+
".json": "json",
|
|
45
|
+
".yaml": "yaml",
|
|
46
|
+
".yml": "yaml",
|
|
47
|
+
".toml": "toml",
|
|
48
|
+
".md": "markdown",
|
|
49
|
+
".sh": "bash",
|
|
50
|
+
".bash": "bash",
|
|
51
|
+
".zsh": "bash",
|
|
52
|
+
".sql": "sql",
|
|
53
|
+
".tf": "terraform",
|
|
54
|
+
".xml": "xml",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ALWAYS_SKIP = {".git", ".agentpack"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def file_hash(path: Path) -> str:
|
|
61
|
+
h = hashlib.sha256()
|
|
62
|
+
with path.open("rb") as f:
|
|
63
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
64
|
+
h.update(chunk)
|
|
65
|
+
return h.hexdigest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_binary(path: Path) -> bool:
|
|
69
|
+
if path.suffix.lower() in BINARY_EXTENSIONS:
|
|
70
|
+
return True
|
|
71
|
+
try:
|
|
72
|
+
chunk = path.read_bytes()[:1024]
|
|
73
|
+
return b"\x00" in chunk
|
|
74
|
+
except OSError:
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def scan(
|
|
79
|
+
root: Path,
|
|
80
|
+
ignore_spec: pathspec.PathSpec,
|
|
81
|
+
max_file_tokens: int = 4000,
|
|
82
|
+
) -> ScanResult:
|
|
83
|
+
packable: list[FileInfo] = []
|
|
84
|
+
ignored: list[FileInfo] = []
|
|
85
|
+
binary: list[FileInfo] = []
|
|
86
|
+
|
|
87
|
+
for abs_path in root.rglob("*"):
|
|
88
|
+
if not abs_path.is_file():
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
rel = abs_path.relative_to(root)
|
|
92
|
+
parts = rel.parts
|
|
93
|
+
|
|
94
|
+
if any(p in ALWAYS_SKIP for p in parts):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
rel_str = str(rel)
|
|
98
|
+
|
|
99
|
+
if is_ignored(ignore_spec, rel_str):
|
|
100
|
+
ignored.append(
|
|
101
|
+
FileInfo(
|
|
102
|
+
path=rel_str,
|
|
103
|
+
abs_path=abs_path,
|
|
104
|
+
size_bytes=abs_path.stat().st_size,
|
|
105
|
+
estimated_tokens=0,
|
|
106
|
+
ignored=True,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
if _is_binary(abs_path):
|
|
112
|
+
size = abs_path.stat().st_size
|
|
113
|
+
lang = LANGUAGE_MAP.get(abs_path.suffix.lower())
|
|
114
|
+
binary.append(
|
|
115
|
+
FileInfo(
|
|
116
|
+
path=rel_str,
|
|
117
|
+
abs_path=abs_path,
|
|
118
|
+
language=lang,
|
|
119
|
+
size_bytes=size,
|
|
120
|
+
estimated_tokens=0,
|
|
121
|
+
binary=True,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
size = abs_path.stat().st_size
|
|
127
|
+
lang = LANGUAGE_MAP.get(abs_path.suffix.lower())
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
text = abs_path.read_text(errors="replace")
|
|
131
|
+
except OSError:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
tokens = estimate_tokens(text)
|
|
135
|
+
too_large = tokens > max_file_tokens
|
|
136
|
+
|
|
137
|
+
packable.append(
|
|
138
|
+
FileInfo(
|
|
139
|
+
path=rel_str,
|
|
140
|
+
abs_path=abs_path,
|
|
141
|
+
language=lang,
|
|
142
|
+
size_bytes=size,
|
|
143
|
+
estimated_tokens=tokens,
|
|
144
|
+
hash=file_hash(abs_path),
|
|
145
|
+
too_large=too_large,
|
|
146
|
+
content=text,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return ScanResult(packable=packable, ignored=ignored, binary=binary)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agentpack.core.merkle import root_hash
|
|
9
|
+
from agentpack.core.models import FileInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
SNAPSHOT_VERSION = 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _snapshots_dir(root: Path) -> Path:
|
|
16
|
+
return root / ".agentpack" / "snapshots"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _latest_path(root: Path) -> Path:
|
|
20
|
+
return _snapshots_dir(root) / "latest.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_snapshot(files: list[FileInfo]) -> dict[str, Any]:
|
|
24
|
+
"""Build a snapshot from packable FileInfo objects. Skips ignored and binary entries defensively."""
|
|
25
|
+
file_data: dict[str, Any] = {}
|
|
26
|
+
hashes: dict[str, str] = {}
|
|
27
|
+
for f in files:
|
|
28
|
+
if f.ignored or f.binary:
|
|
29
|
+
continue
|
|
30
|
+
file_data[f.path] = {
|
|
31
|
+
"hash": f.hash,
|
|
32
|
+
"size_bytes": f.size_bytes,
|
|
33
|
+
"estimated_tokens": f.estimated_tokens,
|
|
34
|
+
"language": f.language,
|
|
35
|
+
}
|
|
36
|
+
if f.hash:
|
|
37
|
+
hashes[f.path] = f.hash
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
"version": SNAPSHOT_VERSION,
|
|
41
|
+
"root_hash": root_hash(hashes),
|
|
42
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
43
|
+
"files": file_data,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save_snapshot(snapshot: dict[str, Any], root: Path) -> None:
|
|
48
|
+
snapshots_dir = _snapshots_dir(root)
|
|
49
|
+
snapshots_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
_latest_path(root).write_text(json.dumps(snapshot, indent=2))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_snapshot(root: Path) -> dict[str, Any] | None:
|
|
54
|
+
path = _latest_path(root)
|
|
55
|
+
if not path.exists():
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
return json.loads(path.read_text())
|
|
59
|
+
except (json.JSONDecodeError, OSError):
|
|
60
|
+
return None
|
|
@@ -0,0 +1,26 @@
|
|
|
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)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Pack repo context and immediately start working on the task. Supports session mode (start once, work normally) and manual pack mode. Reads context and begins helping — no manual piping needed.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# AgentPack
|
|
6
|
+
|
|
7
|
+
Pack repo context and immediately start working on the task.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
/agentpack --task "fix Redis SSE cancellation issue"
|
|
13
|
+
/agentpack --task "add rate limiting to auth endpoints" --mode deep
|
|
14
|
+
/agentpack --task auto # infer task from branch + changed files + git log
|
|
15
|
+
/agentpack init # interactive: prompts for default mode
|
|
16
|
+
/agentpack status
|
|
17
|
+
/agentpack stats
|
|
18
|
+
/agentpack diff
|
|
19
|
+
/agentpack summarize
|
|
20
|
+
/agentpack install
|
|
21
|
+
/agentpack session start
|
|
22
|
+
/agentpack session status
|
|
23
|
+
/agentpack session refresh --task "new task"
|
|
24
|
+
/agentpack session stop
|
|
25
|
+
/agentpack watch
|
|
26
|
+
/agentpack claude
|
|
27
|
+
/agentpack explain --task auto
|
|
28
|
+
/agentpack explain --file src/auth/session.py
|
|
29
|
+
/agentpack explain --omitted
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Session Mode (recommended)
|
|
33
|
+
|
|
34
|
+
If a session is already running (`.agentpack/session.json` exists and `"active": true`):
|
|
35
|
+
|
|
36
|
+
1. Read `.agentpack/context.md` — context is already fresh.
|
|
37
|
+
2. If the user gives a new coding task, write a one-line summary to `.agentpack/task.md`.
|
|
38
|
+
3. Re-read `.agentpack/context.md` after watch mode refreshes it (a few seconds).
|
|
39
|
+
4. Proceed with the task using the context you just read.
|
|
40
|
+
|
|
41
|
+
To start a session:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
agentpack session start # creates session + generates initial context
|
|
45
|
+
agentpack session start --agent claude # specify agent
|
|
46
|
+
agentpack session start --task "fix bug" # set initial task
|
|
47
|
+
|
|
48
|
+
agentpack watch # in another terminal — auto-refreshes on changes
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To check session state:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
agentpack session status # shows active, agent, mode, last refresh, refresh count
|
|
55
|
+
agentpack stats # shows session panel + token stats + top files
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To force a refresh:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
agentpack session refresh
|
|
62
|
+
agentpack session refresh --task "new task description"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
To stop:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
agentpack session stop
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then use normal prompts — context stays current while `watch` is running.
|
|
72
|
+
|
|
73
|
+
## Manual Pack Mode (no session)
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
agentpack pack --agent claude --task "<task>" --mode balanced
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Then read `.agentpack/context.claude.md` in full.
|
|
80
|
+
|
|
81
|
+
## Process
|
|
82
|
+
|
|
83
|
+
### Step 1: Check agentpack is installed
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
agentpack --help 2>/dev/null || pip install agentpack-cli
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Step 2: Initialize if not already done
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
test -f .agentpack/config.toml || agentpack init --yes
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Step 3: Determine workflow
|
|
96
|
+
|
|
97
|
+
**Session active** (`.agentpack/session.json` exists, `"active": true`):
|
|
98
|
+
- Read `.agentpack/context.md`
|
|
99
|
+
- Update `.agentpack/task.md` if task changed
|
|
100
|
+
- Proceed immediately
|
|
101
|
+
|
|
102
|
+
**No session**:
|
|
103
|
+
- Run `agentpack session start` or `agentpack pack --task auto`
|
|
104
|
+
- Read the context file
|
|
105
|
+
- Proceed
|
|
106
|
+
|
|
107
|
+
### Step 4: Immediately start working
|
|
108
|
+
|
|
109
|
+
Using the context you just read:
|
|
110
|
+
|
|
111
|
+
1. **Orient** — state which files are changed and what the key code areas are (2-3 sentences)
|
|
112
|
+
2. **Diagnose or plan** — root cause for bugs, approach for features. Reference specific file:line
|
|
113
|
+
3. **Start working** — edit code, fix the issue, implement the feature
|
|
114
|
+
|
|
115
|
+
Do not say "context pack ready" and stop. Do not tell the user to run more commands.
|
|
116
|
+
|
|
117
|
+
## Stale pack handling
|
|
118
|
+
|
|
119
|
+
If `agentpack status` exits non-zero or context seems unrelated to the task:
|
|
120
|
+
- Run `agentpack session refresh` (if session active)
|
|
121
|
+
- Or run `agentpack pack --task auto` (manual mode)
|
|
122
|
+
- Re-read the context, then proceed
|
|
123
|
+
|
|
124
|
+
Do not ask the user — just refresh and proceed.
|
|
125
|
+
|
|
126
|
+
## Debugging selection
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
agentpack explain --task auto # show ranked file list
|
|
130
|
+
agentpack explain --file src/auth/session.py # per-file score breakdown
|
|
131
|
+
agentpack explain --omitted # see what was excluded and why
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Subcommand routing
|
|
135
|
+
|
|
136
|
+
| User types | Action |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `/agentpack --task "..."` | check session or pack + read + work |
|
|
139
|
+
| `/agentpack` | check session or pack with `--task auto` + read + work |
|
|
140
|
+
| `/agentpack session start` | `agentpack session start` |
|
|
141
|
+
| `/agentpack session status` | `agentpack session status` |
|
|
142
|
+
| `/agentpack session refresh` | `agentpack session refresh` |
|
|
143
|
+
| `/agentpack session stop` | `agentpack session stop` |
|
|
144
|
+
| `/agentpack watch` | `agentpack watch` (foreground, Ctrl+C to stop) |
|
|
145
|
+
| `/agentpack claude` | `agentpack claude` (refresh + launch claude) |
|
|
146
|
+
| `/agentpack init` | `agentpack init` only |
|
|
147
|
+
| `/agentpack status` | check staleness |
|
|
148
|
+
| `/agentpack stats` | session info + token savings |
|
|
149
|
+
| `/agentpack diff` | changed files |
|
|
150
|
+
| `/agentpack summarize` | rebuild offline summary cache |
|
|
151
|
+
| `/agentpack install` | `agentpack install --agent claude` |
|
|
152
|
+
| `/agentpack explain` | show ranked file selection |
|
|
153
|
+
|
|
154
|
+
## Notes
|
|
155
|
+
|
|
156
|
+
- All commands are local — no API calls
|
|
157
|
+
- `--task auto` infers from branch name → changed file paths → recent commit
|
|
158
|
+
- Changed files are highest priority in context
|
|
159
|
+
- Session context files: `.agentpack/context.md` (readable), `.agentpack/context.compact.md` (compact)
|
|
160
|
+
- Never overwrite `.agentignore` or `config.toml` without `--force`
|
|
File without changes
|