skillscout-cli 0.2.0__tar.gz → 0.4.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 (27) hide show
  1. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/PKG-INFO +89 -14
  2. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/README.md +86 -11
  3. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/pyproject.toml +3 -3
  4. skillscout_cli-0.4.0/skillscout/cli.py +254 -0
  5. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout/core.py +40 -12
  6. skillscout_cli-0.4.0/skillscout/history.py +153 -0
  7. skillscout_cli-0.4.0/skillscout/persona.py +103 -0
  8. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout/profile.example.toml +21 -0
  9. skillscout_cli-0.4.0/skillscout/switch.py +169 -0
  10. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout_cli.egg-info/PKG-INFO +89 -14
  11. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout_cli.egg-info/SOURCES.txt +4 -0
  12. skillscout_cli-0.4.0/tests/test_include.py +21 -0
  13. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/tests/test_skillscout.py +178 -1
  14. skillscout_cli-0.2.0/skillscout/cli.py +0 -119
  15. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/LICENSE +0 -0
  16. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/setup.cfg +0 -0
  17. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout/__init__.py +0 -0
  18. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout/github.py +0 -0
  19. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout/index.py +0 -0
  20. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout/scan.py +0 -0
  21. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout/schedule.py +0 -0
  22. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout/score.py +0 -0
  23. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout/skill.py +0 -0
  24. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout_cli.egg-info/dependency_links.txt +0 -0
  25. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout_cli.egg-info/entry_points.txt +0 -0
  26. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout_cli.egg-info/requires.txt +0 -0
  27. {skillscout_cli-0.2.0 → skillscout_cli-0.4.0}/skillscout_cli.egg-info/top_level.txt +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skillscout-cli
3
- Version: 0.2.0
4
- Summary: Keep your agent's skills current: weekly discovery, upstream diffs, safety scans, pinned installs
3
+ Version: 0.4.0
4
+ Summary: Keep your agent's skills current for each kind of work you do: weekly index, personas, scans, pinned installs
5
5
  Author: Akshit Gupta
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://akshit9162.github.io/skillscout
8
8
  Project-URL: Source, https://github.com/akshit9162/skillscout
9
9
  Project-URL: Issues, https://github.com/akshit9162/skillscout/issues
10
10
  Project-URL: Report, https://akshit9162.github.io/skillscout/report.html
11
- Keywords: claude,claude-code,agent-skills,security,supply-chain,llm
11
+ Keywords: claude,claude-code,agent-skills,personas,subagents,security,supply-chain,llm
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Intended Audience :: Developers
@@ -27,13 +27,14 @@ Requires-Dist: pytest; extra == "dev"
27
27
  Dynamic: license-file
28
28
 
29
29
  <p align="center">
30
- <img src="https://raw.githubusercontent.com/akshit9162/skillscout/main/docs/assets/header.svg" width="760" alt="skillscout: keep your agent's skills current, safely" />
30
+ <img src="https://akshit9162.github.io/skillscout/assets/header.svg" width="760" alt="skillscout: keep your agent's skills current, for every kind of work you do" />
31
31
  </p>
32
32
 
33
33
  <p align="center">
34
34
  <a href="https://akshit9162.github.io/skillscout">Website</a> &ensp;·&ensp;
35
35
  <a href="https://akshit9162.github.io/skillscout/report.html">Report: what's inside 6,726 skills</a> &ensp;·&ensp;
36
36
  <a href="#install">Install</a> &ensp;·&ensp;
37
+ <a href="#personas">Personas</a> &ensp;·&ensp;
37
38
  <a href="#a-week-with-skillscout">A week with it</a> &ensp;·&ensp;
38
39
  <a href="#how-it-works">How it works</a> &ensp;·&ensp;
39
40
  <a href="#what-gets-flagged">What gets flagged</a>
@@ -47,7 +48,7 @@ Dynamic: license-file
47
48
  </p>
48
49
 
49
50
  <p align="center">
50
- <img src="https://raw.githubusercontent.com/akshit9162/skillscout/main/docs/assets/demo.svg" width="760" alt="Terminal demo: discover finds 216 recommended skills with 89 flagged high-risk; a high-risk skill is refused without --accept-risk; a low-risk one installs pinned to a commit" />
51
+ <img src="https://akshit9162.github.io/skillscout/assets/demo.svg" width="760" alt="Terminal demo: init drafts keywords from one line; persona add creates a brand persona; discover scores 23,865 skills with a section for the brand persona; persona build writes its agent; claude --agent brand starts it" />
51
52
  </p>
52
53
 
53
54
  ```bash
@@ -60,28 +61,92 @@ AI tooling moves weekly. New skills, plugins and agent workflows land on GitHub
60
61
  installed keep changing upstream, and last month's best practice is already stale. Nobody has time to
61
62
  watch hundreds of repos.
62
63
 
63
- **skillscout keeps your agent current.** Once a week it checks the skill ecosystem against what *you*
64
- work on, tells you what's new and worth having, and shows you exactly what changed in the skills you
65
- already use.
64
+ **skillscout keeps your agent current, for each kind of work you do.** Every week a shared index of every
65
+ public skill it can find (32,360 from 1,173 repos at the last build) is rebuilt. skillscout scores it on your
66
+ machine against what *you* work on, tells you what's new and worth having, and shows you exactly what
67
+ changed in the skills you already use.
68
+
69
+ You probably don't do one kind of work. Coding on Monday, a landing page on Tuesday, investor emails on
70
+ Friday. So skillscout keeps **personas**: one per kind of work, each with its own search and its own Claude
71
+ Code agent, loaded with the best skills for that work and kept up to date as better ones appear.
66
72
 
67
73
  Keeping up fast is only useful if it's safe. A skill is a set of instructions your agent follows
68
74
  **with your permissions**, so skillscout scans every file for dangerous patterns, installs nothing without
69
75
  your approval, pins each install to an exact commit, and re-scans every update before it can replace anything.
70
76
 
71
- In one run over the public ecosystem it found **6,726 skills in 95 repos**. 198 had at least one high-risk flag,
77
+ In a hand-checked run on 27 Oct 2026 over **6,726 skills in 95 repos**, 198 had at least one high-risk flag,
72
78
  mostly `sudo` and `curl | sh` install steps, plus prompt-injection-style text and hidden Unicode.
73
79
  The [report](https://akshit9162.github.io/skillscout/report.html) reads every one of the scariest flags: they were defenders.
74
80
 
75
81
  It's a zero-dependency Python CLI, and it also ships as a skill, so your agent can run the whole loop for you
76
82
  and still ask before every install.
77
83
 
84
+ ## Personas
85
+
86
+ A persona is one side of your work, in your words. skillscout doesn't ship any: yours might be "builder",
87
+ "brand" and "outreach", someone else's "trading" and "thesis".
88
+
89
+ ```text
90
+ You "set up personas for me" (in Claude Code, with the plugin)
91
+ Agent reads your recent sessions on this machine, with your OK (skillscout history)
92
+ → proposes: builder · brand · outreach, each with the sessions it came from
93
+ You keep brand and builder, rename outreach to launches
94
+ Agent skillscout persona add brand --about "D2C marketing: ad copy, landing pages, Instagram reels"
95
+ → keywords drafted from terms skills actually use: instagram reels, ad copy, landing pages...
96
+ skillscout discover → a "For your brand work" section in the report
97
+ skillscout persona build brand → ~/.claude/agents/brand.md, skills: copy, landing-page-design, script...
98
+ You claude --agent brand
99
+ ```
100
+
101
+ ### Switch persona, and the rest stays out of the way
102
+
103
+ Every installed skill's description loads into every Claude Code session, so a big collection crowds out
104
+ the work. Switch to the persona you're in and Claude Code lists only its skills:
105
+
106
+ ```sh
107
+ skillscout persona use brand # or say "switch to brand" to the agent
108
+ # active: brand
109
+ # hid 31: fastapi, postgres-patterns, seo-audit, ...
110
+ # plugins on 1: marketing@knowledge-work-plugins
111
+ # plugins off 2: sales@knowledge-work-plugins, android-skills@android-skills
112
+ skillscout persona use brand builder # several at once
113
+ skillscout persona start brand # switch, then start Claude Code (with brand's agent if built)
114
+ skillscout persona use --core # no persona: only [switch] core and unclaimed skills
115
+ skillscout persona use --clear # everything back as it was
116
+ ```
117
+
118
+ It writes `skillOverrides` and `enabledPlugins` in `~/.claude/settings.json`: nothing moves on disk.
119
+ A persona claims the skills that fit its keywords, plus any you pin with `skills = [...]`, minus `exclude`.
120
+ Plugins switch whole, so you pin them with `plugins = [...]` (`persona add` drafts them from the plugins
121
+ whose skills fit). skillscout restores only what it changed, never overwrites an override you set, and a
122
+ skill you approve later joins whichever personas it fits. A new session picks the switch up; in an open
123
+ one, run `/reload-skills` and `/reload-plugins`.
124
+
125
+ From then on, each weekly report has a section per persona, and `persona build` swaps in a better skill when
126
+ one arrives (after you've read and approved it). `persona build` only ever rewrites the agent's `skills:` list:
127
+ its prompt, tone, tools, model and memory are yours.
128
+
129
+ The CLI works without the agent too:
130
+
131
+ ```sh
132
+ skillscout persona add brand --about "D2C marketing: ad copy, landing pages, Instagram reels"
133
+ skillscout persona list # keywords, best installed skills, what's worth installing
134
+ skillscout persona build --all # write or update ~/.claude/agents/<name>.md
135
+ ```
136
+
137
+ **Your history stays on your machine.** `skillscout history` reads `~/.claude/projects` only when you run it,
138
+ keeps only what you typed (no tool output, no pasted blocks, no links) and sends nothing anywhere. Grouping
139
+ sessions into personas is left to you or the agent in your own session: counting words can't tell short chat
140
+ messages apart reliably, and a wrong persona is worse than none.
141
+
78
142
  ## A week with skillscout
79
143
 
80
144
  Example session (counts vary week to week):
81
145
 
82
146
  ```text
83
147
  Monday 10:00 skillscout discover --notify (weekly, via schedule)
84
- → "4 new, 23 recommended, 11 flagged high-risk"
148
+ → "23 recommended for you (4 new this run), 11 relevant but flagged high-risk"
149
+ → plus a section per persona: "For your brand work", "For your builder work"
85
150
  You skillscout show github/awesome-copilot:eval-driven-dev
86
151
  → flags with file:line, the full SKILL.md, overlaps with what you have
87
152
  You skillscout approve eval-driven-dev → copied, pinned to its commit
@@ -98,17 +163,18 @@ claude plugin marketplace add akshit9162/skillscout
98
163
  claude plugin install skillscout@skillscout
99
164
  ```
100
165
 
101
- Then ask: *"find me skills for my Python and React work"*. The agent installs the CLI, runs the search,
166
+ Then ask: *"find me skills for my Python and React work"*, or *"set up personas for me"*. The agent installs the CLI, runs the search,
102
167
  summarises the report and shows you each skill's flags before asking to install it.
103
168
 
104
169
  ### As a CLI
105
170
 
106
171
  ```sh
107
172
  pipx install skillscout-cli # or: uv tool install skillscout-cli
108
- skillscout init # writes ~/.skillscout/profile.toml
173
+ skillscout init --about "backend in Python, code review, RL evals, Postgres"
109
174
  ```
110
175
 
111
- Edit `[keywords]` in `~/.skillscout/profile.toml` with the words from your own work, then:
176
+ `init` writes `~/.skillscout/profile.toml` and drafts `[keywords]` from your line, using only terms that skills
177
+ actually use. Check them, then:
112
178
 
113
179
  ```sh
114
180
  skillscout discover # score this week's index; writes ~/.skillscout/reports/<date>.md
@@ -167,6 +233,13 @@ High-risk skills need `--accept-risk`, which the bundled skill tells the agent n
167
233
  | `skillscout approve <id> [--update] [--accept-risk] [--to DIR]` | Install and pin. `--to`, or `dir` under `[install]` in your profile, installs into another agent's skills folder |
168
234
  | `skillscout installed` · `skillscout remove <id>` · `skillscout rollback <id>` | Pinned skills · uninstall · undo the last update |
169
235
  | `skillscout updates` | Diff pinned skills against upstream |
236
+ | `skillscout init [--about "..."]` | Write the profile; `--about` drafts your keywords from one line |
237
+ | `skillscout history [--days N]` | A local digest of your recent Claude Code sessions, to draft personas from |
238
+ | `skillscout persona add <name> --about "..." [--keywords "a=3,b=2"]` | Add a persona; keywords drafted from the line unless given |
239
+ | `skillscout persona list` | Each persona: keywords, best installed skills, what's worth installing |
240
+ | `skillscout persona build <name> \| --all [--dry-run]` | Write `~/.claude/agents/<name>.md`, or update only its `skills:` list |
241
+ | `skillscout persona use [<name>...] [--core] [--clear]` | List only these personas' skills and plugins in Claude Code; `--clear` undoes it |
242
+ | `skillscout persona start <name>...` | `persona use`, then start Claude Code |
170
243
  | `skillscout schedule` | Set up weekly discovery: launchd on macOS, a systemd timer on Linux (cron line without systemd), a `schtasks` command on Windows. It prints the one command that turns it on |
171
244
 
172
245
  With the index, `discover` makes no GitHub API calls at all. For `--local` and `index`, set `GITHUB_TOKEN`, or just be logged in with the GitHub CLI (`gh auth login`), for higher API rate limits (5,000 requests an hour instead of 60). Following lists costs one lookup per linked repo: up to 1,500 a run with a token, 40 without; links not yet checked go first, so later runs reach the rest. A token is optional: without it, a rate limit means waiting rather than failing. A weekly run with few upstream changes takes about 25 seconds; the first run takes a few minutes. Scheduled jobs don't see your shell's environment, so add the token to the job if you want it there.
@@ -176,7 +249,9 @@ With the index, `discover` makes no GitHub API calls at all. For `--local` and `
176
249
  - The scanner catches known patterns. It can't prove a skill is safe, so read `show` before you approve.
177
250
  - Relevance is keyword matching against your profile, not semantic search. Tune the weights.
178
251
  - The scanner is pattern rules, deliberately: a skill is text written to steer a model, so a model judging it can be steered by it. The rules make you read; they don't decide.
179
- - Every installed skill's description loads into every agent session. Keep the total to a few dozen.
252
+ - Every installed skill's description loads into every agent session. `persona use` keeps only the active
253
+ personas' skills listed; without it, keep the total to a few dozen.
254
+ - A persona's `skills:` load in full when its agent starts. `max_skills` (default 5) keeps that small.
180
255
 
181
256
  ## Development
182
257
 
@@ -1,11 +1,12 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/akshit9162/skillscout/main/docs/assets/header.svg" width="760" alt="skillscout: keep your agent's skills current, safely" />
2
+ <img src="https://akshit9162.github.io/skillscout/assets/header.svg" width="760" alt="skillscout: keep your agent's skills current, for every kind of work you do" />
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
6
  <a href="https://akshit9162.github.io/skillscout">Website</a> &ensp;·&ensp;
7
7
  <a href="https://akshit9162.github.io/skillscout/report.html">Report: what's inside 6,726 skills</a> &ensp;·&ensp;
8
8
  <a href="#install">Install</a> &ensp;·&ensp;
9
+ <a href="#personas">Personas</a> &ensp;·&ensp;
9
10
  <a href="#a-week-with-skillscout">A week with it</a> &ensp;·&ensp;
10
11
  <a href="#how-it-works">How it works</a> &ensp;·&ensp;
11
12
  <a href="#what-gets-flagged">What gets flagged</a>
@@ -19,7 +20,7 @@
19
20
  </p>
20
21
 
21
22
  <p align="center">
22
- <img src="https://raw.githubusercontent.com/akshit9162/skillscout/main/docs/assets/demo.svg" width="760" alt="Terminal demo: discover finds 216 recommended skills with 89 flagged high-risk; a high-risk skill is refused without --accept-risk; a low-risk one installs pinned to a commit" />
23
+ <img src="https://akshit9162.github.io/skillscout/assets/demo.svg" width="760" alt="Terminal demo: init drafts keywords from one line; persona add creates a brand persona; discover scores 23,865 skills with a section for the brand persona; persona build writes its agent; claude --agent brand starts it" />
23
24
  </p>
24
25
 
25
26
  ```bash
@@ -32,28 +33,92 @@ AI tooling moves weekly. New skills, plugins and agent workflows land on GitHub
32
33
  installed keep changing upstream, and last month's best practice is already stale. Nobody has time to
33
34
  watch hundreds of repos.
34
35
 
35
- **skillscout keeps your agent current.** Once a week it checks the skill ecosystem against what *you*
36
- work on, tells you what's new and worth having, and shows you exactly what changed in the skills you
37
- already use.
36
+ **skillscout keeps your agent current, for each kind of work you do.** Every week a shared index of every
37
+ public skill it can find (32,360 from 1,173 repos at the last build) is rebuilt. skillscout scores it on your
38
+ machine against what *you* work on, tells you what's new and worth having, and shows you exactly what
39
+ changed in the skills you already use.
40
+
41
+ You probably don't do one kind of work. Coding on Monday, a landing page on Tuesday, investor emails on
42
+ Friday. So skillscout keeps **personas**: one per kind of work, each with its own search and its own Claude
43
+ Code agent, loaded with the best skills for that work and kept up to date as better ones appear.
38
44
 
39
45
  Keeping up fast is only useful if it's safe. A skill is a set of instructions your agent follows
40
46
  **with your permissions**, so skillscout scans every file for dangerous patterns, installs nothing without
41
47
  your approval, pins each install to an exact commit, and re-scans every update before it can replace anything.
42
48
 
43
- In one run over the public ecosystem it found **6,726 skills in 95 repos**. 198 had at least one high-risk flag,
49
+ In a hand-checked run on 27 Oct 2026 over **6,726 skills in 95 repos**, 198 had at least one high-risk flag,
44
50
  mostly `sudo` and `curl | sh` install steps, plus prompt-injection-style text and hidden Unicode.
45
51
  The [report](https://akshit9162.github.io/skillscout/report.html) reads every one of the scariest flags: they were defenders.
46
52
 
47
53
  It's a zero-dependency Python CLI, and it also ships as a skill, so your agent can run the whole loop for you
48
54
  and still ask before every install.
49
55
 
56
+ ## Personas
57
+
58
+ A persona is one side of your work, in your words. skillscout doesn't ship any: yours might be "builder",
59
+ "brand" and "outreach", someone else's "trading" and "thesis".
60
+
61
+ ```text
62
+ You "set up personas for me" (in Claude Code, with the plugin)
63
+ Agent reads your recent sessions on this machine, with your OK (skillscout history)
64
+ → proposes: builder · brand · outreach, each with the sessions it came from
65
+ You keep brand and builder, rename outreach to launches
66
+ Agent skillscout persona add brand --about "D2C marketing: ad copy, landing pages, Instagram reels"
67
+ → keywords drafted from terms skills actually use: instagram reels, ad copy, landing pages...
68
+ skillscout discover → a "For your brand work" section in the report
69
+ skillscout persona build brand → ~/.claude/agents/brand.md, skills: copy, landing-page-design, script...
70
+ You claude --agent brand
71
+ ```
72
+
73
+ ### Switch persona, and the rest stays out of the way
74
+
75
+ Every installed skill's description loads into every Claude Code session, so a big collection crowds out
76
+ the work. Switch to the persona you're in and Claude Code lists only its skills:
77
+
78
+ ```sh
79
+ skillscout persona use brand # or say "switch to brand" to the agent
80
+ # active: brand
81
+ # hid 31: fastapi, postgres-patterns, seo-audit, ...
82
+ # plugins on 1: marketing@knowledge-work-plugins
83
+ # plugins off 2: sales@knowledge-work-plugins, android-skills@android-skills
84
+ skillscout persona use brand builder # several at once
85
+ skillscout persona start brand # switch, then start Claude Code (with brand's agent if built)
86
+ skillscout persona use --core # no persona: only [switch] core and unclaimed skills
87
+ skillscout persona use --clear # everything back as it was
88
+ ```
89
+
90
+ It writes `skillOverrides` and `enabledPlugins` in `~/.claude/settings.json`: nothing moves on disk.
91
+ A persona claims the skills that fit its keywords, plus any you pin with `skills = [...]`, minus `exclude`.
92
+ Plugins switch whole, so you pin them with `plugins = [...]` (`persona add` drafts them from the plugins
93
+ whose skills fit). skillscout restores only what it changed, never overwrites an override you set, and a
94
+ skill you approve later joins whichever personas it fits. A new session picks the switch up; in an open
95
+ one, run `/reload-skills` and `/reload-plugins`.
96
+
97
+ From then on, each weekly report has a section per persona, and `persona build` swaps in a better skill when
98
+ one arrives (after you've read and approved it). `persona build` only ever rewrites the agent's `skills:` list:
99
+ its prompt, tone, tools, model and memory are yours.
100
+
101
+ The CLI works without the agent too:
102
+
103
+ ```sh
104
+ skillscout persona add brand --about "D2C marketing: ad copy, landing pages, Instagram reels"
105
+ skillscout persona list # keywords, best installed skills, what's worth installing
106
+ skillscout persona build --all # write or update ~/.claude/agents/<name>.md
107
+ ```
108
+
109
+ **Your history stays on your machine.** `skillscout history` reads `~/.claude/projects` only when you run it,
110
+ keeps only what you typed (no tool output, no pasted blocks, no links) and sends nothing anywhere. Grouping
111
+ sessions into personas is left to you or the agent in your own session: counting words can't tell short chat
112
+ messages apart reliably, and a wrong persona is worse than none.
113
+
50
114
  ## A week with skillscout
51
115
 
52
116
  Example session (counts vary week to week):
53
117
 
54
118
  ```text
55
119
  Monday 10:00 skillscout discover --notify (weekly, via schedule)
56
- → "4 new, 23 recommended, 11 flagged high-risk"
120
+ → "23 recommended for you (4 new this run), 11 relevant but flagged high-risk"
121
+ → plus a section per persona: "For your brand work", "For your builder work"
57
122
  You skillscout show github/awesome-copilot:eval-driven-dev
58
123
  → flags with file:line, the full SKILL.md, overlaps with what you have
59
124
  You skillscout approve eval-driven-dev → copied, pinned to its commit
@@ -70,17 +135,18 @@ claude plugin marketplace add akshit9162/skillscout
70
135
  claude plugin install skillscout@skillscout
71
136
  ```
72
137
 
73
- Then ask: *"find me skills for my Python and React work"*. The agent installs the CLI, runs the search,
138
+ Then ask: *"find me skills for my Python and React work"*, or *"set up personas for me"*. The agent installs the CLI, runs the search,
74
139
  summarises the report and shows you each skill's flags before asking to install it.
75
140
 
76
141
  ### As a CLI
77
142
 
78
143
  ```sh
79
144
  pipx install skillscout-cli # or: uv tool install skillscout-cli
80
- skillscout init # writes ~/.skillscout/profile.toml
145
+ skillscout init --about "backend in Python, code review, RL evals, Postgres"
81
146
  ```
82
147
 
83
- Edit `[keywords]` in `~/.skillscout/profile.toml` with the words from your own work, then:
148
+ `init` writes `~/.skillscout/profile.toml` and drafts `[keywords]` from your line, using only terms that skills
149
+ actually use. Check them, then:
84
150
 
85
151
  ```sh
86
152
  skillscout discover # score this week's index; writes ~/.skillscout/reports/<date>.md
@@ -139,6 +205,13 @@ High-risk skills need `--accept-risk`, which the bundled skill tells the agent n
139
205
  | `skillscout approve <id> [--update] [--accept-risk] [--to DIR]` | Install and pin. `--to`, or `dir` under `[install]` in your profile, installs into another agent's skills folder |
140
206
  | `skillscout installed` · `skillscout remove <id>` · `skillscout rollback <id>` | Pinned skills · uninstall · undo the last update |
141
207
  | `skillscout updates` | Diff pinned skills against upstream |
208
+ | `skillscout init [--about "..."]` | Write the profile; `--about` drafts your keywords from one line |
209
+ | `skillscout history [--days N]` | A local digest of your recent Claude Code sessions, to draft personas from |
210
+ | `skillscout persona add <name> --about "..." [--keywords "a=3,b=2"]` | Add a persona; keywords drafted from the line unless given |
211
+ | `skillscout persona list` | Each persona: keywords, best installed skills, what's worth installing |
212
+ | `skillscout persona build <name> \| --all [--dry-run]` | Write `~/.claude/agents/<name>.md`, or update only its `skills:` list |
213
+ | `skillscout persona use [<name>...] [--core] [--clear]` | List only these personas' skills and plugins in Claude Code; `--clear` undoes it |
214
+ | `skillscout persona start <name>...` | `persona use`, then start Claude Code |
142
215
  | `skillscout schedule` | Set up weekly discovery: launchd on macOS, a systemd timer on Linux (cron line without systemd), a `schtasks` command on Windows. It prints the one command that turns it on |
143
216
 
144
217
  With the index, `discover` makes no GitHub API calls at all. For `--local` and `index`, set `GITHUB_TOKEN`, or just be logged in with the GitHub CLI (`gh auth login`), for higher API rate limits (5,000 requests an hour instead of 60). Following lists costs one lookup per linked repo: up to 1,500 a run with a token, 40 without; links not yet checked go first, so later runs reach the rest. A token is optional: without it, a rate limit means waiting rather than failing. A weekly run with few upstream changes takes about 25 seconds; the first run takes a few minutes. Scheduled jobs don't see your shell's environment, so add the token to the job if you want it there.
@@ -148,7 +221,9 @@ With the index, `discover` makes no GitHub API calls at all. For `--local` and `
148
221
  - The scanner catches known patterns. It can't prove a skill is safe, so read `show` before you approve.
149
222
  - Relevance is keyword matching against your profile, not semantic search. Tune the weights.
150
223
  - The scanner is pattern rules, deliberately: a skill is text written to steer a model, so a model judging it can be steered by it. The rules make you read; they don't decide.
151
- - Every installed skill's description loads into every agent session. Keep the total to a few dozen.
224
+ - Every installed skill's description loads into every agent session. `persona use` keeps only the active
225
+ personas' skills listed; without it, keep the total to a few dozen.
226
+ - A persona's `skills:` load in full when its agent starts. `max_skills` (default 5) keeps that small.
152
227
 
153
228
  ## Development
154
229
 
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skillscout-cli"
7
- version = "0.2.0"
8
- description = "Keep your agent's skills current: weekly discovery, upstream diffs, safety scans, pinned installs"
7
+ version = "0.4.0"
8
+ description = "Keep your agent's skills current for each kind of work you do: weekly index, personas, scans, pinned installs"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [{ name = "Akshit Gupta" }]
14
- keywords = ["claude", "claude-code", "agent-skills", "security", "supply-chain", "llm"]
14
+ keywords = ["claude", "claude-code", "agent-skills", "personas", "subagents", "security", "supply-chain", "llm"]
15
15
  classifiers = [
16
16
  "Development Status :: 4 - Beta",
17
17
  "Environment :: Console",
@@ -0,0 +1,254 @@
1
+ """skillscout command line."""
2
+
3
+ import argparse
4
+ import json
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from . import __doc__ as ABOUT, core, github, history, index, persona, schedule, switch
11
+
12
+ EXAMPLE = Path(__file__).resolve().parent / "profile.example.toml"
13
+ USER_PROFILE = Path.home() / ".skillscout" / "profile.toml"
14
+
15
+
16
+ def notify(text: str) -> None:
17
+ if sys.platform == "darwin" and shutil.which("osascript"):
18
+ subprocess.run(["osascript", "-e", f'display notification "{text}" with title "skillscout"'], check=False)
19
+ elif shutil.which("notify-send"):
20
+ subprocess.run(["notify-send", "skillscout", text], check=False)
21
+
22
+
23
+ def main(argv: list[str] | None = None) -> None:
24
+ p = argparse.ArgumentParser(prog="skillscout", description=ABOUT)
25
+ p.add_argument("--profile", type=Path, default=None, help="default: ~/.skillscout/profile.toml, else the example")
26
+ sub = p.add_subparsers(dest="cmd", required=True)
27
+ it = sub.add_parser("init", help="write ~/.skillscout/profile.toml")
28
+ it.add_argument("--about", help="one line about your work; becomes your keywords (e.g. \"backend in Python, RL evals, Postgres\")")
29
+ d = sub.add_parser("discover", help="score this week's shared index against your profile and write a report")
30
+ d.add_argument("--local", action="store_true", help="crawl and scan GitHub on this machine instead of using the index")
31
+ d.add_argument("--full", action="store_true", help="with --local: ignore caches, fetch every repo and re-scan every skill")
32
+ d.add_argument("--notify", action="store_true", help="desktop notification with the summary (macOS, or Linux with notify-send)")
33
+ ls = sub.add_parser("list", help="show scored candidates")
34
+ ls.add_argument("--all", action="store_true", help="include zero-relevance skills")
35
+ ls.add_argument("-n", type=int, default=30)
36
+ ix = sub.add_parser("index", help="crawl and scan the ecosystem and write the shared index (what the weekly job runs)")
37
+ ix.add_argument("--out", type=Path, default=Path("index.json.gz"))
38
+ ix.add_argument("--full", action="store_true", help="ignore caches: fetch every repo and re-scan every skill")
39
+ sh = sub.add_parser("show", help="fetch a candidate, scan it here, print its flags and full SKILL.md")
40
+ sh.add_argument("id")
41
+ ap = sub.add_parser("approve", help="install one skill (into ~/.claude/skills unless [install] dir says otherwise) and pin it")
42
+ ap.add_argument("id")
43
+ ap.add_argument("--to", type=Path, help="install into this skills folder instead, e.g. another agent's")
44
+ ap.add_argument("--accept-risk", action="store_true", help="install despite high-risk flags")
45
+ ap.add_argument("--update", action="store_true", help="replace a copy skillscout installed earlier")
46
+ rm = sub.add_parser("remove", help="uninstall a skill skillscout installed")
47
+ rm.add_argument("id")
48
+ rb = sub.add_parser("rollback", help="restore the copy the last `approve --update` replaced")
49
+ rb.add_argument("id")
50
+ sub.add_parser("installed", help="show pinned skills")
51
+ sub.add_parser("updates", help="diff pinned skills against upstream; applies nothing")
52
+ sub.add_parser("schedule", help="set up weekly discovery for this OS (you turn it on)")
53
+ hi = sub.add_parser("history", help="summarise your recent Claude Code sessions, read locally, to draft personas from")
54
+ hi.add_argument("--days", type=int, default=60)
55
+ pe = sub.add_parser("persona", help="kinds of work, each with its own keywords and an agent kept current")
56
+ psub = pe.add_subparsers(dest="action", required=True)
57
+ pa = psub.add_parser("add", help="add a persona from one line about that work")
58
+ pa.add_argument("name")
59
+ pa.add_argument("--about", required=True, help="what this side of your work is, in your words")
60
+ pa.add_argument("--keywords", help="instead of drafting them: \"cold email=4,investor update=3\"")
61
+ pa.add_argument("--agent", help="agent file name, if not the persona's name")
62
+ psub.add_parser("list", help="each persona, its keywords, and the installed skills that fit it")
63
+ pb = psub.add_parser("build", help="write or update the persona's agent in ~/.claude/agents (only its skills: list)")
64
+ pb.add_argument("name", nargs="?")
65
+ pb.add_argument("--all", action="store_true")
66
+ pb.add_argument("--dry-run", action="store_true")
67
+ pu = psub.add_parser("use", help="list only these personas' skills and plugins in Claude Code (no names: show what's active)")
68
+ pu.add_argument("names", nargs="*")
69
+ pu.add_argument("--clear", action="store_true", help="undo every change skillscout made: all skills and plugins as before")
70
+ pu.add_argument("--core", action="store_true", help="no persona: hide every persona's skills and plugins, keep [switch] core")
71
+ ps = psub.add_parser("start", help="`persona use` these personas, then start Claude Code")
72
+ ps.add_argument("names", nargs="+")
73
+ a = p.parse_args(argv)
74
+
75
+ paths = core.Paths()
76
+ if a.cmd == "init":
77
+ if USER_PROFILE.exists():
78
+ raise SystemExit(f"{USER_PROFILE} already exists")
79
+ text = EXAMPLE.read_text(encoding="utf-8")
80
+ if a.about:
81
+ kw = history.keywords_from(a.about, vocabulary(core.load_profile(EXAMPLE), paths))
82
+ if not kw:
83
+ raise SystemExit("none of those words appear in skill descriptions; try more specific ones")
84
+ head, rest = text.split("[keywords]\n", 1)
85
+ lines = rest.splitlines(keepends=True)
86
+ keep = next(i for i, line in enumerate(lines) if not line.startswith('"') and line.strip())
87
+ tail = "".join(lines[keep:])
88
+ text = head + "[keywords]\n" + f"# drafted from: {a.about}\n" + "".join(f"{json.dumps(k)} = {v}\n" for k, v in kw.items()) + "\n" + tail
89
+ USER_PROFILE.parent.mkdir(parents=True, exist_ok=True)
90
+ USER_PROFILE.write_text(text, encoding="utf-8")
91
+ print(f"wrote {USER_PROFILE}; " + ("check the drafted [keywords]" if a.about else "edit [keywords] to match your work")
92
+ + ", then run `skillscout discover`")
93
+ return
94
+ profile = core.load_profile(a.profile or (USER_PROFILE if USER_PROFILE.exists() else EXAMPLE))
95
+ if folder := getattr(a, "to", None) or profile.get("install", {}).get("dir"):
96
+ paths.skills = Path(folder).expanduser()
97
+ cands = core._read(paths.candidates, [])
98
+
99
+ if a.cmd == "discover":
100
+ url = profile.get("index", {}).get("url", index.URL)
101
+ if a.local or not url:
102
+ cands = core.discover(profile, paths, full=a.full)
103
+ else:
104
+ try:
105
+ cands = core.discover_from_index(profile, paths, url, profile.get("index", {}).get("verify", False))
106
+ except index.Unavailable as e:
107
+ raise SystemExit(str(e))
108
+ out, summary = core.report(cands, profile, paths)
109
+ print(f"{summary}\nreport: {out}")
110
+ if a.notify:
111
+ notify(summary)
112
+ elif a.cmd == "index":
113
+ records = core.crawl(profile["search"], paths, full=a.full)
114
+ data = index.build(records, a.out, core.RULES)
115
+ print(f"wrote {a.out}: {len(data['skills'])} skills from {len({r['repo'] for r in records})} repos")
116
+ elif a.cmd == "list":
117
+ rows = [c for c in cands if a.all or c["relevance"] > 0][: a.n]
118
+ for c in rows:
119
+ tag = "installed" if c["installed"] else ("REC" if core.recommended(c, profile) else "")
120
+ print(f"{c['score']:.2f} {c['risk'] or '-':6} {tag:9} {c['id']}")
121
+ elif a.cmd == "show":
122
+ c = core.ensure_local(core.find(cands, a.id), paths)
123
+ print(json.dumps({k: c[k] for k in ("id", "score", "stars", "license", "commit", "risk", "matched", "conflicts")}, indent=2))
124
+ for f in c["flags"]:
125
+ print(f" [{f['level']}] {f['reason']} — {f['where']}")
126
+ src = core.source_dir(c, paths)
127
+ print(f"\nfiles: {', '.join(sorted(str(x.relative_to(src)) for x in src.rglob('*') if x.is_file()))}\n")
128
+ print((src / "SKILL.md").read_text(encoding="utf-8", errors="ignore"))
129
+ elif a.cmd == "approve":
130
+ c = core.find(cands, a.id)
131
+ print(f"installed {c['id']} -> {core.approve(c, paths, a.accept_risk, a.update, fetch=github)}")
132
+ if on := switch.active(paths): # a new skill for an inactive persona stays out of view
133
+ names = [n for n in on if n != switch.CORE]
134
+ say_switch(switch.use(profile, paths, names, core.installed_skills(paths), not names), on, quiet=True)
135
+ elif a.cmd == "remove":
136
+ print(f"removed {core.remove(a.id, paths)}")
137
+ elif a.cmd == "rollback":
138
+ print(f"restored the previous {a.id} -> {core.rollback(a.id, paths)}")
139
+ elif a.cmd == "installed":
140
+ for cid, e in core._read(paths.lock, {}).items():
141
+ print(f"{cid} @{e['commit'][:10]} {e['installed_at']}")
142
+ elif a.cmd == "updates":
143
+ found = core.updates(paths)
144
+ for cid, commit, diff in found:
145
+ print(f"\n=== {cid} -> {commit[:10]}\n{diff}")
146
+ print(f"\n{len(found)} pinned skill(s) changed upstream." + (" Re-run `approve <id> --update` after reading the diff." if found else ""))
147
+ elif a.cmd == "history":
148
+ text = history.digest(history.episodes(days=a.days))
149
+ print(text or "no Claude Code history found in ~/.claude/projects")
150
+ print(f"(read from ~/.claude/projects on this machine, last {a.days} days; nothing is sent anywhere)")
151
+ elif a.cmd == "persona":
152
+ run_persona(a, profile, paths, cands)
153
+ elif a.cmd == "schedule":
154
+ files, message = schedule.here(paths.state)
155
+ schedule.write(files)
156
+ print(message)
157
+
158
+
159
+ def say_switch(done: dict, names: list[str], quiet: bool = False) -> None:
160
+ parts = [f"{label} {len(done[k])}: {', '.join(done[k][:8])}{' ...' if len(done[k]) > 8 else ''}"
161
+ for k, label in (("hidden", "hid"), ("shown", "showed"), ("plugins_off", "plugins off"), ("plugins_on", "plugins on"))
162
+ if done[k]]
163
+ if done["kept_yours"]:
164
+ parts.append(f"left your own skillOverrides alone for {', '.join(done['kept_yours'])}")
165
+ if quiet and not parts:
166
+ return
167
+ print((f"active: {', '.join(names)}" if names else "cleared: every skill and plugin as before skillscout switched them"))
168
+ for line in parts:
169
+ print(f" {line}")
170
+ if parts:
171
+ print("A new Claude Code session picks this up; in an open one, run /reload-skills and /reload-plugins.")
172
+
173
+
174
+ def start_claude(names: list[str], known: dict, paths: core.Paths) -> None:
175
+ claude = shutil.which("claude")
176
+ if not claude:
177
+ raise SystemExit("claude isn't on PATH; the switch is done, start Claude Code yourself")
178
+ cmd = [claude]
179
+ agent = known[names[0]]["agent"] if len(names) == 1 else None
180
+ if agent and (paths.agents / f"{agent}.md").exists():
181
+ cmd += ["--agent", agent]
182
+ sys.exit(subprocess.run(cmd, check=False).returncode)
183
+
184
+
185
+ def vocabulary(profile: dict, paths: core.Paths) -> dict[str, int]:
186
+ """Terms skills use, from the cached index (downloaded first if there isn't one)."""
187
+ try:
188
+ if not paths.index.exists():
189
+ index.fetch(profile.get("index", {}).get("url") or index.URL, paths.index)
190
+ return history.vocabulary(index.load(paths.index)["skills"])
191
+ except index.Unavailable as e:
192
+ raise SystemExit(f"{e}\n(drafting keywords needs the skill index; or write them by hand)")
193
+
194
+
195
+ def run_persona(a, profile: dict, paths: core.Paths, cands: list[dict]) -> None:
196
+ known = persona.personas(profile)
197
+ if a.action == "add":
198
+ if a.keywords:
199
+ kw = {k.strip(): int(v) for k, v in (pair.rsplit("=", 1) for pair in a.keywords.split(","))}
200
+ else:
201
+ kw = history.keywords_from(a.about, vocabulary(profile, paths))
202
+ path = a.profile or USER_PROFILE
203
+ if not path.exists():
204
+ raise SystemExit(f"no profile at {path}; run `skillscout init` first")
205
+ print(persona.add(path, a.name, a.about, kw, a.agent, switch.draft_plugins(paths, kw)))
206
+ print(f"\nadded to {path}. Next: `skillscout discover`, then `skillscout persona build {a.name}`")
207
+ return
208
+ installed = core.installed_skills(paths)
209
+ if a.action in ("use", "start"):
210
+ if a.action == "use" and not a.names and not a.clear and not a.core:
211
+ on = switch.active(paths)
212
+ print(f"active: {', '.join(on)}" if on else "no persona active: Claude Code lists every skill")
213
+ return
214
+ names = [] if a.action == "use" and a.clear else a.names
215
+ core_only = a.action == "use" and a.core and not a.clear
216
+ say_switch(switch.use(profile, paths, names, installed, core_only), names or ([switch.CORE] if core_only else []))
217
+ if a.action == "start":
218
+ start_claude(names, known, paths)
219
+ return
220
+ if not known:
221
+ raise SystemExit("no personas yet: add one with `skillscout persona add <name> --about \"...\"`")
222
+ if a.action == "list":
223
+ on = switch.active(paths)
224
+ by = switch.claims(profile, switch.own_skills(installed))
225
+ for name, p in known.items():
226
+ fits = persona.fitting(installed, p["keywords"], p["max_skills"])
227
+ agent = paths.agents / f"{p['agent']}.md"
228
+ print(f"{name}: {p['description']}\n keywords: {', '.join(p['keywords'])}\n"
229
+ f" agent: {agent}{'' if agent.exists() else ' (not built yet)'}\n"
230
+ f" best installed skills: {', '.join(n for n, _ in fits) or 'none fit yet'}\n"
231
+ f" switches: {len(by[name])} of your skills, plugins: {', '.join(p['plugins']) or 'none'}"
232
+ + (" (active)" if name in on else ""))
233
+ pending = [c for c in cands if core.recommended(c, profile) and c.get("personas", {}).get(name, 0) >= 0.3][:3]
234
+ if pending:
235
+ print(f" worth installing: {', '.join(c['id'] for c in pending)}")
236
+ return
237
+ names = list(known) if a.all else [a.name] if a.name in known else None
238
+ if not names:
239
+ raise SystemExit(f"name a persona ({', '.join(known)}) or pass --all")
240
+ for name in names:
241
+ p = known[name]
242
+ skills = [n for n, _ in persona.fitting(installed, p["keywords"], p["max_skills"])]
243
+ path = paths.agents / f"{p['agent']}.md"
244
+ if a.dry_run:
245
+ print(f"{path}: skills would be {', '.join(skills) or '(none fit yet)'}")
246
+ continue
247
+ what, added, dropped = persona.write_agent(path, p["agent"], p["description"], skills)
248
+ print(f"{what} {path}" + (f"; added {', '.join(added)}" if added else "") + (f"; dropped {', '.join(dropped)}" if dropped else "")
249
+ + ("" if added or dropped or what == "created" else "; skills unchanged"))
250
+ print("Start one with `claude --agent <name>`, or ask for it by name in any session.")
251
+
252
+
253
+ if __name__ == "__main__":
254
+ main()