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.
- agent_persona-0.1.0/.gitignore +15 -0
- agent_persona-0.1.0/PKG-INFO +188 -0
- agent_persona-0.1.0/README.md +173 -0
- agent_persona-0.1.0/assets/logo.svg +9 -0
- agent_persona-0.1.0/pyproject.toml +40 -0
- agent_persona-0.1.0/src/agent_persona/__init__.py +1 -0
- agent_persona-0.1.0/src/agent_persona/analyzers/__init__.py +0 -0
- agent_persona-0.1.0/src/agent_persona/analyzers/preference_extractor.py +22 -0
- agent_persona-0.1.0/src/agent_persona/analyzers/stack_detector.py +69 -0
- agent_persona-0.1.0/src/agent_persona/analyzers/style_detector.py +26 -0
- agent_persona-0.1.0/src/agent_persona/cli.py +250 -0
- agent_persona-0.1.0/src/agent_persona/collectors/__init__.py +0 -0
- agent_persona-0.1.0/src/agent_persona/collectors/explicit_feedback.py +63 -0
- agent_persona-0.1.0/src/agent_persona/collectors/file_patterns.py +110 -0
- agent_persona-0.1.0/src/agent_persona/collectors/shell_history.py +56 -0
- agent_persona-0.1.0/src/agent_persona/collectors/transcript.py +121 -0
- agent_persona-0.1.0/src/agent_persona/compiler.py +56 -0
- agent_persona-0.1.0/src/agent_persona/hooks/__init__.py +0 -0
- agent_persona-0.1.0/src/agent_persona/hooks/file_hook.py +86 -0
- agent_persona-0.1.0/src/agent_persona/hooks/session_start_hook.py +41 -0
- agent_persona-0.1.0/src/agent_persona/hooks/stop_hook.py +97 -0
- agent_persona-0.1.0/src/agent_persona/injector.py +28 -0
- agent_persona-0.1.0/src/agent_persona/installer.py +149 -0
- agent_persona-0.1.0/src/agent_persona/markers.py +86 -0
- agent_persona-0.1.0/src/agent_persona/models.py +52 -0
- agent_persona-0.1.0/src/agent_persona/profile.py +152 -0
- agent_persona-0.1.0/src/agent_persona/tui.py +153 -0
- agent_persona-0.1.0/tests/__init__.py +0 -0
- agent_persona-0.1.0/tests/conftest.py +26 -0
- agent_persona-0.1.0/tests/test_analyzers.py +186 -0
- agent_persona-0.1.0/tests/test_cli.py +429 -0
- agent_persona-0.1.0/tests/test_collectors.py +544 -0
- agent_persona-0.1.0/tests/test_hooks.py +308 -0
- agent_persona-0.1.0/tests/test_injector.py +75 -0
- agent_persona-0.1.0/tests/test_installer.py +127 -0
- agent_persona-0.1.0/tests/test_markers.py +179 -0
- agent_persona-0.1.0/tests/test_profile.py +205 -0
- agent_persona-0.1.0/tests/test_tui.py +188 -0
|
@@ -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
|
+
[](https://pypi.org/project/agent-persona/)
|
|
25
|
+
[](#)
|
|
26
|
+
[](#)
|
|
27
|
+
[](#)
|
|
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
|
+
[](https://pypi.org/project/agent-persona/)
|
|
10
|
+
[](#)
|
|
11
|
+
[](#)
|
|
12
|
+
[](#)
|
|
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"
|
|
File without changes
|
|
@@ -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
|
+
)
|