agent-persona 0.1.0__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 (38) hide show
  1. agent_persona-0.1.0/.gitignore +15 -0
  2. agent_persona-0.1.0/PKG-INFO +188 -0
  3. agent_persona-0.1.0/README.md +173 -0
  4. agent_persona-0.1.0/assets/logo.svg +9 -0
  5. agent_persona-0.1.0/pyproject.toml +40 -0
  6. agent_persona-0.1.0/src/agent_persona/__init__.py +1 -0
  7. agent_persona-0.1.0/src/agent_persona/analyzers/__init__.py +0 -0
  8. agent_persona-0.1.0/src/agent_persona/analyzers/preference_extractor.py +22 -0
  9. agent_persona-0.1.0/src/agent_persona/analyzers/stack_detector.py +69 -0
  10. agent_persona-0.1.0/src/agent_persona/analyzers/style_detector.py +26 -0
  11. agent_persona-0.1.0/src/agent_persona/cli.py +250 -0
  12. agent_persona-0.1.0/src/agent_persona/collectors/__init__.py +0 -0
  13. agent_persona-0.1.0/src/agent_persona/collectors/explicit_feedback.py +63 -0
  14. agent_persona-0.1.0/src/agent_persona/collectors/file_patterns.py +110 -0
  15. agent_persona-0.1.0/src/agent_persona/collectors/shell_history.py +56 -0
  16. agent_persona-0.1.0/src/agent_persona/collectors/transcript.py +121 -0
  17. agent_persona-0.1.0/src/agent_persona/compiler.py +56 -0
  18. agent_persona-0.1.0/src/agent_persona/hooks/__init__.py +0 -0
  19. agent_persona-0.1.0/src/agent_persona/hooks/file_hook.py +86 -0
  20. agent_persona-0.1.0/src/agent_persona/hooks/session_start_hook.py +41 -0
  21. agent_persona-0.1.0/src/agent_persona/hooks/stop_hook.py +97 -0
  22. agent_persona-0.1.0/src/agent_persona/injector.py +28 -0
  23. agent_persona-0.1.0/src/agent_persona/installer.py +149 -0
  24. agent_persona-0.1.0/src/agent_persona/markers.py +86 -0
  25. agent_persona-0.1.0/src/agent_persona/models.py +52 -0
  26. agent_persona-0.1.0/src/agent_persona/profile.py +152 -0
  27. agent_persona-0.1.0/src/agent_persona/tui.py +153 -0
  28. agent_persona-0.1.0/tests/__init__.py +0 -0
  29. agent_persona-0.1.0/tests/conftest.py +26 -0
  30. agent_persona-0.1.0/tests/test_analyzers.py +186 -0
  31. agent_persona-0.1.0/tests/test_cli.py +429 -0
  32. agent_persona-0.1.0/tests/test_collectors.py +544 -0
  33. agent_persona-0.1.0/tests/test_hooks.py +308 -0
  34. agent_persona-0.1.0/tests/test_injector.py +75 -0
  35. agent_persona-0.1.0/tests/test_installer.py +127 -0
  36. agent_persona-0.1.0/tests/test_markers.py +179 -0
  37. agent_persona-0.1.0/tests/test_profile.py +205 -0
  38. agent_persona-0.1.0/tests/test_tui.py +188 -0
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pyo
4
+ .venv/
5
+ venv/
6
+ .env
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .coverage
11
+ htmlcov/
12
+ .pytest_cache/
13
+ .DS_Store
14
+ *.tmp
15
+ *.bak
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-persona
3
+ Version: 0.1.0
4
+ Summary: Local-first personalization layer for Claude CLI — learns how you work and injects that context into every session
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: click>=8.1
8
+ Requires-Dist: filelock>=3.12
9
+ Requires-Dist: questionary>=2.0
10
+ Requires-Dist: rich>=13.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
13
+ Requires-Dist: pytest>=8.0; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ <div align="center">
17
+
18
+ <img src="assets/logo.svg" width="100" alt="agent-persona logo" />
19
+
20
+ # agent-persona
21
+
22
+ *Personalise your Claude CLI to your persona — learned from how you actually work.*
23
+
24
+ [![PyPI](https://img.shields.io/pypi/v/agent-persona?color=blue&label=pypi)](https://pypi.org/project/agent-persona/)
25
+ [![Python](https://img.shields.io/badge/python-3.11+-blue)](#)
26
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](#)
27
+ [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)](#)
28
+
29
+ </div>
30
+
31
+ ---
32
+
33
+ Claude is powerful out of the box. But it doesn't know *you*. It doesn't know your preferred stack, your coding style, or that you always want root-cause analysis before a fix. Every session, you're a stranger.
34
+
35
+ **agent-persona changes that.** It reads your Claude session logs, the files you edit, and your shell history — builds a global profile of your persona on this machine — and silently injects that context into every Claude session from then on. Claude stops being a generic assistant and starts behaving like one that knows how you work.
36
+
37
+ No configuration. No manual writing. It learns from what you do.
38
+
39
+ ---
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pipx install agent-persona
45
+ agent-persona install
46
+ ```
47
+
48
+ From the next session on, it's building your persona.
49
+
50
+ ---
51
+
52
+ ## How your persona is built
53
+
54
+ agent-persona watches four things:
55
+
56
+ | Source | What it captures |
57
+ |---|---|
58
+ | **Claude session logs** | Parsed from `~/.claude/` after each session ends |
59
+ | **Files you edit** | Every `Write` / `Edit` in Claude CLI is logged asynchronously |
60
+ | **Shell history** | Read from `~/.zsh_history` or `~/.bash_history` at analysis time |
61
+ | **Your own words** | Phrases like `"I prefer..."`, `"always use..."`, `"never do..."` extracted from your side of every conversation |
62
+
63
+ From those sources, it builds a profile across five dimensions:
64
+
65
+ - **Languages** — which languages you actually use, weighted by frequency across sessions
66
+ - **Frameworks** — inferred from imports, file extensions, and install commands
67
+ - **Tools** — CLI tools, build systems, databases that appear repeatedly in your workflow
68
+ - **Preferences** — explicit things you've told Claude, confidence-scored and time-decayed so stale preferences fade out
69
+ - **Coding style** — indent preference from your actual files; test frequency from how often you write tests
70
+
71
+ Everything is stored in `~/.agent-persona/profile.json` — plain JSON, readable, portable, yours.
72
+
73
+ ---
74
+
75
+ ## How it gets into Claude
76
+
77
+ Three hooks wire into `~/.claude/settings.json`:
78
+
79
+ **`SessionStart`** — injects your compiled persona as hidden context before you type a word. Claude already knows how you work.
80
+
81
+ **`Stop` (×2)** — when a session ends:
82
+ - A command hook runs the full rule-based pipeline, updates `profile.json`, regenerates `context.md`
83
+ - A prompt hook gives the already-running Claude instance a free richer pass — no extra API calls, no extra cost
84
+
85
+ **`PostToolUse`** — every file edit is logged asynchronously in the background. Never blocks your session.
86
+
87
+ ---
88
+
89
+ ## Your profile, globally
90
+
91
+ The profile lives at `~/.agent-persona/` — not inside any project. It spans every Claude session on this machine, regardless of what you're working on.
92
+
93
+ ```
94
+ ~/.agent-persona/
95
+ ├── profile.json # your persona — single source of truth
96
+ ├── state.json # tracks which sessions have been processed
97
+ ├── history/
98
+ │ └── files_edited.jsonl # rolling 90-day log of edited files
99
+ └── generated/
100
+ └── context.md # compiled persona injected at session start
101
+ ```
102
+
103
+ `profile.json` is plain JSON — readable, portable, auditable:
104
+
105
+ ```json
106
+ {
107
+ "version": 1,
108
+ "stack": {
109
+ "languages": { "python": 45, "typescript": 30 },
110
+ "frameworks": { "fastapi": 20, "pytest": 18 },
111
+ "tools": { "docker": 15, "git": 50 }
112
+ },
113
+ "preferences": [
114
+ { "text": "root-cause analysis before fixes", "confidence": 0.9, "source": "rule" },
115
+ { "text": "always include tradeoffs", "confidence": 0.95, "source": "llm" }
116
+ ],
117
+ "coding_style": { "indent": "spaces", "indent_size": 4, "test_frequency": "high" },
118
+ "sessions_processed": 47
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Commands
125
+
126
+ | Command | What it does |
127
+ |---|---|
128
+ | `agent-persona install` | Register hooks, create `~/.agent-persona/` |
129
+ | `agent-persona uninstall` | Remove hooks, leave profile intact |
130
+ | `agent-persona customize` | Interactively set what Claude should know about you and how to respond |
131
+ | `agent-persona status` | Show your current persona: stack, preferences, sessions processed |
132
+ | `agent-persona analyze` | Re-run analysis on demand |
133
+ | `agent-persona apply [--dry-run]` | Write persona into `~/.claude/CLAUDE.md` as a static fallback |
134
+ | `agent-persona export [path]` | Export your profile to take to another machine |
135
+ | `agent-persona import [path]` | Merge an exported profile into this machine's profile |
136
+ | `agent-persona reset --confirm` | Wipe profile and start fresh |
137
+ | `agent-persona doctor` | Check install health |
138
+
139
+ ---
140
+
141
+ ## Taking your persona to a new machine
142
+
143
+ ```bash
144
+ # old machine
145
+ agent-persona export ~/persona.json
146
+
147
+ # new machine
148
+ agent-persona install
149
+ agent-persona import ~/persona.json
150
+ ```
151
+
152
+ The profiles merge — nothing is lost from either machine.
153
+
154
+ ---
155
+
156
+ ## Design decisions
157
+
158
+ **No LLM API calls from Python.** All rule-based analysis runs in pure Python — fast, deterministic, free. The richer LLM extraction piggybacks on the Claude session that's already ending via a `type: "prompt"` Stop hook. Zero extra cost.
159
+
160
+ **Preferences decay.** Each preference carries a `last_seen` timestamp. Old ones fade: `score × 0.9 ^ (days_since_seen / 30)`. What you get stays relevant to how you work *now*.
161
+
162
+ **Crash-safe writes.** Every write is atomic (`write to .tmp → rename`). A crashed Stop hook is caught up automatically on the next clean session.
163
+
164
+ **Non-destructive CLAUDE.md injection.** The `apply` command writes between `AGENT-PERSONA:START` and `AGENT-PERSONA:END` markers. Everything outside is untouched.
165
+
166
+ **Sensitive data never stored.** API keys, tokens, and passwords are stripped before anything reaches `profile.json`. Only file metadata is logged — never file contents.
167
+
168
+ ---
169
+
170
+ ## What it doesn't do
171
+
172
+ - No cloud — your persona never leaves this machine unless you export it
173
+ - No per-project profiles in v1 — one global persona across all work
174
+ - No Fish shell history yet
175
+
176
+ ---
177
+
178
+ ## Requirements
179
+
180
+ - Python 3.11+
181
+ - [Claude CLI](https://claude.ai/code) installed and authenticated
182
+ - macOS, Linux, or Windows
183
+
184
+ ---
185
+
186
+ ## License
187
+
188
+ MIT
@@ -0,0 +1,173 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/logo.svg" width="100" alt="agent-persona logo" />
4
+
5
+ # agent-persona
6
+
7
+ *Personalise your Claude CLI to your persona — learned from how you actually work.*
8
+
9
+ [![PyPI](https://img.shields.io/pypi/v/agent-persona?color=blue&label=pypi)](https://pypi.org/project/agent-persona/)
10
+ [![Python](https://img.shields.io/badge/python-3.11+-blue)](#)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](#)
12
+ [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)](#)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ Claude is powerful out of the box. But it doesn't know *you*. It doesn't know your preferred stack, your coding style, or that you always want root-cause analysis before a fix. Every session, you're a stranger.
19
+
20
+ **agent-persona changes that.** It reads your Claude session logs, the files you edit, and your shell history — builds a global profile of your persona on this machine — and silently injects that context into every Claude session from then on. Claude stops being a generic assistant and starts behaving like one that knows how you work.
21
+
22
+ No configuration. No manual writing. It learns from what you do.
23
+
24
+ ---
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pipx install agent-persona
30
+ agent-persona install
31
+ ```
32
+
33
+ From the next session on, it's building your persona.
34
+
35
+ ---
36
+
37
+ ## How your persona is built
38
+
39
+ agent-persona watches four things:
40
+
41
+ | Source | What it captures |
42
+ |---|---|
43
+ | **Claude session logs** | Parsed from `~/.claude/` after each session ends |
44
+ | **Files you edit** | Every `Write` / `Edit` in Claude CLI is logged asynchronously |
45
+ | **Shell history** | Read from `~/.zsh_history` or `~/.bash_history` at analysis time |
46
+ | **Your own words** | Phrases like `"I prefer..."`, `"always use..."`, `"never do..."` extracted from your side of every conversation |
47
+
48
+ From those sources, it builds a profile across five dimensions:
49
+
50
+ - **Languages** — which languages you actually use, weighted by frequency across sessions
51
+ - **Frameworks** — inferred from imports, file extensions, and install commands
52
+ - **Tools** — CLI tools, build systems, databases that appear repeatedly in your workflow
53
+ - **Preferences** — explicit things you've told Claude, confidence-scored and time-decayed so stale preferences fade out
54
+ - **Coding style** — indent preference from your actual files; test frequency from how often you write tests
55
+
56
+ Everything is stored in `~/.agent-persona/profile.json` — plain JSON, readable, portable, yours.
57
+
58
+ ---
59
+
60
+ ## How it gets into Claude
61
+
62
+ Three hooks wire into `~/.claude/settings.json`:
63
+
64
+ **`SessionStart`** — injects your compiled persona as hidden context before you type a word. Claude already knows how you work.
65
+
66
+ **`Stop` (×2)** — when a session ends:
67
+ - A command hook runs the full rule-based pipeline, updates `profile.json`, regenerates `context.md`
68
+ - A prompt hook gives the already-running Claude instance a free richer pass — no extra API calls, no extra cost
69
+
70
+ **`PostToolUse`** — every file edit is logged asynchronously in the background. Never blocks your session.
71
+
72
+ ---
73
+
74
+ ## Your profile, globally
75
+
76
+ The profile lives at `~/.agent-persona/` — not inside any project. It spans every Claude session on this machine, regardless of what you're working on.
77
+
78
+ ```
79
+ ~/.agent-persona/
80
+ ├── profile.json # your persona — single source of truth
81
+ ├── state.json # tracks which sessions have been processed
82
+ ├── history/
83
+ │ └── files_edited.jsonl # rolling 90-day log of edited files
84
+ └── generated/
85
+ └── context.md # compiled persona injected at session start
86
+ ```
87
+
88
+ `profile.json` is plain JSON — readable, portable, auditable:
89
+
90
+ ```json
91
+ {
92
+ "version": 1,
93
+ "stack": {
94
+ "languages": { "python": 45, "typescript": 30 },
95
+ "frameworks": { "fastapi": 20, "pytest": 18 },
96
+ "tools": { "docker": 15, "git": 50 }
97
+ },
98
+ "preferences": [
99
+ { "text": "root-cause analysis before fixes", "confidence": 0.9, "source": "rule" },
100
+ { "text": "always include tradeoffs", "confidence": 0.95, "source": "llm" }
101
+ ],
102
+ "coding_style": { "indent": "spaces", "indent_size": 4, "test_frequency": "high" },
103
+ "sessions_processed": 47
104
+ }
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Commands
110
+
111
+ | Command | What it does |
112
+ |---|---|
113
+ | `agent-persona install` | Register hooks, create `~/.agent-persona/` |
114
+ | `agent-persona uninstall` | Remove hooks, leave profile intact |
115
+ | `agent-persona customize` | Interactively set what Claude should know about you and how to respond |
116
+ | `agent-persona status` | Show your current persona: stack, preferences, sessions processed |
117
+ | `agent-persona analyze` | Re-run analysis on demand |
118
+ | `agent-persona apply [--dry-run]` | Write persona into `~/.claude/CLAUDE.md` as a static fallback |
119
+ | `agent-persona export [path]` | Export your profile to take to another machine |
120
+ | `agent-persona import [path]` | Merge an exported profile into this machine's profile |
121
+ | `agent-persona reset --confirm` | Wipe profile and start fresh |
122
+ | `agent-persona doctor` | Check install health |
123
+
124
+ ---
125
+
126
+ ## Taking your persona to a new machine
127
+
128
+ ```bash
129
+ # old machine
130
+ agent-persona export ~/persona.json
131
+
132
+ # new machine
133
+ agent-persona install
134
+ agent-persona import ~/persona.json
135
+ ```
136
+
137
+ The profiles merge — nothing is lost from either machine.
138
+
139
+ ---
140
+
141
+ ## Design decisions
142
+
143
+ **No LLM API calls from Python.** All rule-based analysis runs in pure Python — fast, deterministic, free. The richer LLM extraction piggybacks on the Claude session that's already ending via a `type: "prompt"` Stop hook. Zero extra cost.
144
+
145
+ **Preferences decay.** Each preference carries a `last_seen` timestamp. Old ones fade: `score × 0.9 ^ (days_since_seen / 30)`. What you get stays relevant to how you work *now*.
146
+
147
+ **Crash-safe writes.** Every write is atomic (`write to .tmp → rename`). A crashed Stop hook is caught up automatically on the next clean session.
148
+
149
+ **Non-destructive CLAUDE.md injection.** The `apply` command writes between `AGENT-PERSONA:START` and `AGENT-PERSONA:END` markers. Everything outside is untouched.
150
+
151
+ **Sensitive data never stored.** API keys, tokens, and passwords are stripped before anything reaches `profile.json`. Only file metadata is logged — never file contents.
152
+
153
+ ---
154
+
155
+ ## What it doesn't do
156
+
157
+ - No cloud — your persona never leaves this machine unless you export it
158
+ - No per-project profiles in v1 — one global persona across all work
159
+ - No Fish shell history yet
160
+
161
+ ---
162
+
163
+ ## Requirements
164
+
165
+ - Python 3.11+
166
+ - [Claude CLI](https://claude.ai/code) installed and authenticated
167
+ - macOS, Linux, or Windows
168
+
169
+ ---
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
2
+ <circle cx="60" cy="60" r="54" fill="none" stroke="#E6EDF3" stroke-width="3"/>
3
+ <circle cx="60" cy="46" r="18" fill="#E6EDF3"/>
4
+ <ellipse cx="60" cy="88" rx="26" ry="20" fill="#E6EDF3"/>
5
+ <circle cx="60" cy="46" r="6" fill="none" stroke="#0D1117" stroke-width="1.8"/>
6
+ <circle cx="60" cy="46" r="10" fill="none" stroke="#0D1117" stroke-width="1.4"/>
7
+ <circle cx="60" cy="46" r="14" fill="none" stroke="#0D1117" stroke-width="1"/>
8
+ <circle cx="94" cy="26" r="4" fill="#58A6FF"/>
9
+ </svg>
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agent-persona"
7
+ version = "0.1.0"
8
+ description = "Local-first personalization layer for Claude CLI — learns how you work and injects that context into every session"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "click>=8.1",
14
+ "rich>=13.0",
15
+ "filelock>=3.12",
16
+ "questionary>=2.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ agent-persona = "agent_persona.cli:main"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/agent_persona"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
27
+ pythonpath = ["src"]
28
+
29
+ [tool.coverage.run]
30
+ source = ["agent_persona"]
31
+ omit = ["tests/*"]
32
+
33
+ [tool.coverage.report]
34
+ fail_under = 80
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "pytest>=8.0",
39
+ "pytest-cov>=5.0",
40
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from agent_persona.collectors.explicit_feedback import extract_preferences
4
+ from agent_persona.models import Preference
5
+
6
+ _MAX_PREFERENCES = 20
7
+
8
+
9
+ def extract_all_preferences(
10
+ user_messages: list[str],
11
+ now: str,
12
+ ) -> tuple[Preference, ...]:
13
+ raw = extract_preferences(user_messages, now)
14
+ seen: dict[str, Preference] = {}
15
+ for pref in raw:
16
+ key = pref.text.lower().strip()
17
+ if key not in seen:
18
+ seen[key] = pref
19
+
20
+ deduped = list(seen.values())
21
+ deduped.sort(key=lambda p: p.confidence, reverse=True)
22
+ return tuple(deduped[:_MAX_PREFERENCES])
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ LANGUAGE_SIGNALS: dict[str, set[str]] = {
4
+ "python": {".py", "pip", "uv ", "poetry", "pytest", "python3", "python "},
5
+ "typescript": {".ts", ".tsx", "tsc ", "ts-node"},
6
+ "javascript": {".js", ".mjs", "node ", "nodemon"},
7
+ "go": {".go", "go build", "go test", "go run"},
8
+ "rust": {".rs", "cargo build", "cargo test"},
9
+ "java": {".java", "mvn ", "gradle "},
10
+ "ruby": {".rb", "bundle exec", "rails "},
11
+ "bash": {".sh", "#!/bin/bash", "#!/bin/zsh"},
12
+ }
13
+
14
+ FRAMEWORK_SIGNALS: dict[str, set[str]] = {
15
+ "react": {"useState", "useEffect", ".jsx", ".tsx", "import React", "react-dom"},
16
+ "fastapi": {"fastapi", "from fastapi", "uvicorn"},
17
+ "django": {"django", "manage.py", "django.db"},
18
+ "express": {"express()", "app.get(", "app.post("},
19
+ "nextjs": {"next/router", "getServerSideProps", "next.config"},
20
+ "vue": {".vue", "createApp(", "defineComponent"},
21
+ }
22
+
23
+ TOOL_SIGNALS: dict[str, set[str]] = {
24
+ "docker": {"dockerfile", "docker-compose", "docker build", "docker run"},
25
+ "git": {"git commit", "git push", "git pull", "git merge"},
26
+ "npm": {"npm install", "npm run", "npm ci"},
27
+ "yarn": {"yarn add", "yarn install", "yarn run"},
28
+ "pnpm": {"pnpm add", "pnpm install"},
29
+ "mysql": {".sql", "mysql ", "create table", "select * from"},
30
+ "mongo": {"mongodb", "mongoose", "db.collection"},
31
+ "postgres": {"psql ", "pg_dump", "postgresql"},
32
+ "redis": {"redis-cli", "redis.Redis("},
33
+ "aws": {"aws s3", "aws ec2", "boto3", "aws lambda"},
34
+ }
35
+
36
+
37
+ def detect_stack(
38
+ tool_signals: list[str],
39
+ commands: list[str],
40
+ extensions: dict[str, int],
41
+ ) -> dict[str, dict[str, int]]:
42
+ all_text = tool_signals + commands
43
+ ext_text = [f".{ext}" for ext, count in extensions.items() for _ in range(count)]
44
+ combined = all_text + ext_text
45
+
46
+ languages = _score_category(LANGUAGE_SIGNALS, combined)
47
+ frameworks = _score_category(FRAMEWORK_SIGNALS, combined)
48
+ tools = _score_category(TOOL_SIGNALS, combined)
49
+
50
+ return {"languages": languages, "frameworks": frameworks, "tools": tools}
51
+
52
+
53
+ def _score_category(
54
+ signal_map: dict[str, set[str]],
55
+ corpus: list[str],
56
+ ) -> dict[str, int]:
57
+ scores: dict[str, int] = {}
58
+ corpus_lower = [item.lower() for item in corpus]
59
+
60
+ for name, signals in signal_map.items():
61
+ total = 0
62
+ for signal in signals:
63
+ sig_lower = signal.lower()
64
+ matches = sum(1 for item in corpus_lower if sig_lower in item)
65
+ total += min(matches, 5)
66
+ if total > 0:
67
+ scores[name] = total
68
+
69
+ return scores
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from agent_persona.models import CodingStyle
4
+
5
+
6
+ def detect_style(file_signals: dict) -> CodingStyle:
7
+ indent = file_signals.get("indent", "unknown")
8
+ indent_size = file_signals.get("indent_size", 4)
9
+ test_file_count = file_signals.get("test_file_count", 0)
10
+ total_file_count = file_signals.get("total_file_count", 0)
11
+
12
+ ratio = test_file_count / max(total_file_count, 1)
13
+ if ratio > 0.25:
14
+ test_frequency = "high"
15
+ elif ratio > 0.10:
16
+ test_frequency = "medium"
17
+ elif ratio > 0:
18
+ test_frequency = "low"
19
+ else:
20
+ test_frequency = "unknown"
21
+
22
+ return CodingStyle(
23
+ indent=indent,
24
+ indent_size=indent_size,
25
+ test_frequency=test_frequency,
26
+ )