skillscout-cli 0.1.0__tar.gz → 0.2.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 (22) hide show
  1. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/PKG-INFO +20 -13
  2. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/README.md +19 -12
  3. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/pyproject.toml +1 -1
  4. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout/cli.py +32 -10
  5. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout/core.py +203 -54
  6. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout/github.py +6 -5
  7. skillscout_cli-0.2.0/skillscout/index.py +89 -0
  8. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout/profile.example.toml +11 -2
  9. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout/scan.py +5 -4
  10. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout/schedule.py +1 -1
  11. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout/score.py +26 -3
  12. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout/skill.py +16 -6
  13. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout_cli.egg-info/PKG-INFO +20 -13
  14. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout_cli.egg-info/SOURCES.txt +1 -0
  15. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/tests/test_skillscout.py +169 -25
  16. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/LICENSE +0 -0
  17. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/setup.cfg +0 -0
  18. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout/__init__.py +0 -0
  19. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout_cli.egg-info/dependency_links.txt +0 -0
  20. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout_cli.egg-info/entry_points.txt +0 -0
  21. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout_cli.egg-info/requires.txt +0 -0
  22. {skillscout_cli-0.1.0 → skillscout_cli-0.2.0}/skillscout_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skillscout-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Keep your agent's skills current: weekly discovery, upstream diffs, safety scans, pinned installs
5
5
  Author: Akshit Gupta
6
6
  License-Expression: MIT
@@ -53,7 +53,7 @@ Dynamic: license-file
53
53
  ```bash
54
54
  claude plugin marketplace add akshit9162/skillscout && claude plugin install skillscout@skillscout
55
55
  # or the CLI on its own
56
- pipx install git+https://github.com/akshit9162/skillscout
56
+ pipx install skillscout-cli
57
57
  ```
58
58
 
59
59
  AI tooling moves weekly. New skills, plugins and agent workflows land on GitHub every day, the ones you
@@ -104,33 +104,37 @@ summarises the report and shows you each skill's flags before asking to install
104
104
  ### As a CLI
105
105
 
106
106
  ```sh
107
- pipx install git+https://github.com/akshit9162/skillscout # or: uv tool install git+https://...
107
+ pipx install skillscout-cli # or: uv tool install skillscout-cli
108
108
  skillscout init # writes ~/.skillscout/profile.toml
109
109
  ```
110
110
 
111
111
  Edit `[keywords]` in `~/.skillscout/profile.toml` with the words from your own work, then:
112
112
 
113
113
  ```sh
114
- skillscout discover # search, scan, score; writes ~/.skillscout/reports/<date>.md
114
+ skillscout discover # score this week's index; writes ~/.skillscout/reports/<date>.md
115
115
  skillscout list # ranked; REC = recommended
116
116
  skillscout show <id> # every flag plus the full SKILL.md, before you decide
117
117
  skillscout approve <id> # copy into ~/.claude/skills and pin the commit
118
118
  ```
119
119
 
120
- `<id>` is `owner/repo:skill-name`. The bare skill name works when it's unique.
120
+ `<id>` is `owner/repo:skill-name`. The bare skill name works when it's unique. If one repo has two skills with the same name, the second is `owner/repo:path/to/skill`.
121
121
 
122
122
  ## How it works
123
123
 
124
+ The crawl is the same for everyone, so it runs once a week in this repo's [index workflow](.github/workflows/index.yml) and is published as `index.json.gz`. `skillscout discover` downloads that one file (seconds, no GitHub token, your keywords never leave your machine) and scores it locally. `show` and `approve` fetch the skill and scan it again on your machine, so nothing is installed on the index's word. `discover --local` runs the whole crawl yourself instead.
125
+
126
+ Each published index carries a signed [build attestation](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations): proof that this repo's index workflow built that exact file. Check one with `gh attestation verify ~/.skillscout/index.json.gz --repo akshit9162/skillscout`, or set `verify = true` under `[index]` and `discover` checks every download and refuses one that fails.
127
+
124
128
  | Step | What happens |
125
129
  |---|---|
126
- | **Discover** | Runs your GitHub searches (topics like `claude-skills`, `agent-skills`) plus your trusted repos, then follows every repo linked from curated lists (your `lists`, plus any `awesome-*` repo it finds). Drops archived, stale (no push in a year), unpopular and huge repos. |
130
+ | **Discover** (weekly job, or `--local`) | Runs your GitHub searches (topics like `claude-skills`, `agent-skills`) plus your trusted repos, then follows every repo linked from curated lists (your `lists`, plus any `awesome-*` repo it finds). Drops archived, stale (no push in a year), unpopular and huge repos. |
127
131
  | **Clone** | Sparse, shallow clones: only folders that contain a `SKILL.md`, minus media and archives, 16 repos at a time. Repos with no push since the last run aren't fetched again. Repo code is never run. |
128
132
  | **Scan** | Static checks on every file in the skill, reported with file and line ([below](#what-gets-flagged)). A skill whose folder (git tree hash) and the scanner's rules are both unchanged keeps its last result instead of being read again. |
129
- | **Score** | 65% relevance to your keywords, 20% stars, 15% freshness, plus a bonus for trusted repos. |
133
+ | **Score** (your machine) | 65% relevance to your keywords (phrases match in order, singular or plural), 20% stars, 15% freshness, plus a bonus for trusted repos. |
130
134
  | **Conflicts** | Compared against your `~/.claude/skills` and plugin skills: same name, or a description close enough that both would fire on the same prompt. |
131
135
  | **Report** | What's new since your last run, recommended skills, and relevant ones that are blocked and why. |
132
- | **Approve** | You pick one. It's copied (symlinks dropped) and pinned in `~/.skillscout/skills.lock.json` with its commit and a content hash. |
133
- | **Update** | `skillscout updates` diffs each pinned skill against upstream and applies nothing. Taking an update re-runs the scan. |
136
+ | **Approve** | You pick one. It's fetched and scanned again on your machine (that scan decides), then copied (symlinks dropped) and pinned in `~/.skillscout/skills.lock.json` with its commit and a content hash. |
137
+ | **Update** | `skillscout updates` diffs each pinned skill against upstream and applies nothing. Taking an update re-runs the scan, builds the new copy beside the old one and swaps them; if any step fails, the old copy and its pin stay. `skillscout rollback <id>` puts back the version an update replaced. |
134
138
 
135
139
  ## What gets flagged
136
140
 
@@ -146,6 +150,7 @@ High-risk skills need `--accept-risk`, which the bundled skill tells the agent n
146
150
 
147
151
  - Nothing installs without an explicit `approve`.
148
152
  - `approve` never overwrites a folder skillscout didn't install, and never follows symlinks.
153
+ - A skill can't name its way out of the skills folder: a `name:` like `../../x` falls back to the folder's own name, and a path that leads outside its repo is refused. The install name comes from the `SKILL.md` fetched on your machine, not from the index.
149
154
  - Every install is pinned to a commit and a content hash.
150
155
  - Updates are a diff first, and re-scanned before they replace anything. The test suite includes an upstream
151
156
  update that slips in `sudo rm -rf /`, and checks that re-approval refuses it.
@@ -155,20 +160,22 @@ High-risk skills need `--accept-risk`, which the bundled skill tells the agent n
155
160
  | Command | Does |
156
161
  |---|---|
157
162
  | `skillscout init` | Copy the example profile to `~/.skillscout/profile.toml` |
158
- | `skillscout discover [--full] [--notify]` | Search, scan, score, write the report. `--full` ignores the caches; `--notify` sends a desktop notification (macOS, or Linux with `notify-send`) |
163
+ | `skillscout discover [--local [--full]] [--notify]` | Score the weekly index against your profile and write the report. `--local` crawls and scans GitHub on your machine instead; `--full` then ignores the caches; `--notify` sends a desktop notification (macOS, or Linux with `notify-send`) |
164
+ | `skillscout index [--out FILE]` | Crawl and scan the ecosystem and write the shared index (what the weekly job runs) |
159
165
  | `skillscout list [--all] [-n N]` | Ranked candidates |
160
166
  | `skillscout show <id>` | Flags, conflicts and the full SKILL.md |
161
- | `skillscout approve <id> [--update] [--accept-risk]` | Install and pin |
162
- | `skillscout installed` · `skillscout remove <id>` | Pinned skills · uninstall |
167
+ | `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
+ | `skillscout installed` · `skillscout remove <id>` · `skillscout rollback <id>` | Pinned skills · uninstall · undo the last update |
163
169
  | `skillscout updates` | Diff pinned skills against upstream |
164
170
  | `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 |
165
171
 
166
- 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 300 a run with a token, 40 without. 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.
172
+ 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.
167
173
 
168
174
  ## Limits
169
175
 
170
176
  - The scanner catches known patterns. It can't prove a skill is safe, so read `show` before you approve.
171
177
  - Relevance is keyword matching against your profile, not semantic search. Tune the weights.
178
+ - 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.
172
179
  - Every installed skill's description loads into every agent session. Keep the total to a few dozen.
173
180
 
174
181
  ## Development
@@ -25,7 +25,7 @@
25
25
  ```bash
26
26
  claude plugin marketplace add akshit9162/skillscout && claude plugin install skillscout@skillscout
27
27
  # or the CLI on its own
28
- pipx install git+https://github.com/akshit9162/skillscout
28
+ pipx install skillscout-cli
29
29
  ```
30
30
 
31
31
  AI tooling moves weekly. New skills, plugins and agent workflows land on GitHub every day, the ones you
@@ -76,33 +76,37 @@ summarises the report and shows you each skill's flags before asking to install
76
76
  ### As a CLI
77
77
 
78
78
  ```sh
79
- pipx install git+https://github.com/akshit9162/skillscout # or: uv tool install git+https://...
79
+ pipx install skillscout-cli # or: uv tool install skillscout-cli
80
80
  skillscout init # writes ~/.skillscout/profile.toml
81
81
  ```
82
82
 
83
83
  Edit `[keywords]` in `~/.skillscout/profile.toml` with the words from your own work, then:
84
84
 
85
85
  ```sh
86
- skillscout discover # search, scan, score; writes ~/.skillscout/reports/<date>.md
86
+ skillscout discover # score this week's index; writes ~/.skillscout/reports/<date>.md
87
87
  skillscout list # ranked; REC = recommended
88
88
  skillscout show <id> # every flag plus the full SKILL.md, before you decide
89
89
  skillscout approve <id> # copy into ~/.claude/skills and pin the commit
90
90
  ```
91
91
 
92
- `<id>` is `owner/repo:skill-name`. The bare skill name works when it's unique.
92
+ `<id>` is `owner/repo:skill-name`. The bare skill name works when it's unique. If one repo has two skills with the same name, the second is `owner/repo:path/to/skill`.
93
93
 
94
94
  ## How it works
95
95
 
96
+ The crawl is the same for everyone, so it runs once a week in this repo's [index workflow](.github/workflows/index.yml) and is published as `index.json.gz`. `skillscout discover` downloads that one file (seconds, no GitHub token, your keywords never leave your machine) and scores it locally. `show` and `approve` fetch the skill and scan it again on your machine, so nothing is installed on the index's word. `discover --local` runs the whole crawl yourself instead.
97
+
98
+ Each published index carries a signed [build attestation](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations): proof that this repo's index workflow built that exact file. Check one with `gh attestation verify ~/.skillscout/index.json.gz --repo akshit9162/skillscout`, or set `verify = true` under `[index]` and `discover` checks every download and refuses one that fails.
99
+
96
100
  | Step | What happens |
97
101
  |---|---|
98
- | **Discover** | Runs your GitHub searches (topics like `claude-skills`, `agent-skills`) plus your trusted repos, then follows every repo linked from curated lists (your `lists`, plus any `awesome-*` repo it finds). Drops archived, stale (no push in a year), unpopular and huge repos. |
102
+ | **Discover** (weekly job, or `--local`) | Runs your GitHub searches (topics like `claude-skills`, `agent-skills`) plus your trusted repos, then follows every repo linked from curated lists (your `lists`, plus any `awesome-*` repo it finds). Drops archived, stale (no push in a year), unpopular and huge repos. |
99
103
  | **Clone** | Sparse, shallow clones: only folders that contain a `SKILL.md`, minus media and archives, 16 repos at a time. Repos with no push since the last run aren't fetched again. Repo code is never run. |
100
104
  | **Scan** | Static checks on every file in the skill, reported with file and line ([below](#what-gets-flagged)). A skill whose folder (git tree hash) and the scanner's rules are both unchanged keeps its last result instead of being read again. |
101
- | **Score** | 65% relevance to your keywords, 20% stars, 15% freshness, plus a bonus for trusted repos. |
105
+ | **Score** (your machine) | 65% relevance to your keywords (phrases match in order, singular or plural), 20% stars, 15% freshness, plus a bonus for trusted repos. |
102
106
  | **Conflicts** | Compared against your `~/.claude/skills` and plugin skills: same name, or a description close enough that both would fire on the same prompt. |
103
107
  | **Report** | What's new since your last run, recommended skills, and relevant ones that are blocked and why. |
104
- | **Approve** | You pick one. It's copied (symlinks dropped) and pinned in `~/.skillscout/skills.lock.json` with its commit and a content hash. |
105
- | **Update** | `skillscout updates` diffs each pinned skill against upstream and applies nothing. Taking an update re-runs the scan. |
108
+ | **Approve** | You pick one. It's fetched and scanned again on your machine (that scan decides), then copied (symlinks dropped) and pinned in `~/.skillscout/skills.lock.json` with its commit and a content hash. |
109
+ | **Update** | `skillscout updates` diffs each pinned skill against upstream and applies nothing. Taking an update re-runs the scan, builds the new copy beside the old one and swaps them; if any step fails, the old copy and its pin stay. `skillscout rollback <id>` puts back the version an update replaced. |
106
110
 
107
111
  ## What gets flagged
108
112
 
@@ -118,6 +122,7 @@ High-risk skills need `--accept-risk`, which the bundled skill tells the agent n
118
122
 
119
123
  - Nothing installs without an explicit `approve`.
120
124
  - `approve` never overwrites a folder skillscout didn't install, and never follows symlinks.
125
+ - A skill can't name its way out of the skills folder: a `name:` like `../../x` falls back to the folder's own name, and a path that leads outside its repo is refused. The install name comes from the `SKILL.md` fetched on your machine, not from the index.
121
126
  - Every install is pinned to a commit and a content hash.
122
127
  - Updates are a diff first, and re-scanned before they replace anything. The test suite includes an upstream
123
128
  update that slips in `sudo rm -rf /`, and checks that re-approval refuses it.
@@ -127,20 +132,22 @@ High-risk skills need `--accept-risk`, which the bundled skill tells the agent n
127
132
  | Command | Does |
128
133
  |---|---|
129
134
  | `skillscout init` | Copy the example profile to `~/.skillscout/profile.toml` |
130
- | `skillscout discover [--full] [--notify]` | Search, scan, score, write the report. `--full` ignores the caches; `--notify` sends a desktop notification (macOS, or Linux with `notify-send`) |
135
+ | `skillscout discover [--local [--full]] [--notify]` | Score the weekly index against your profile and write the report. `--local` crawls and scans GitHub on your machine instead; `--full` then ignores the caches; `--notify` sends a desktop notification (macOS, or Linux with `notify-send`) |
136
+ | `skillscout index [--out FILE]` | Crawl and scan the ecosystem and write the shared index (what the weekly job runs) |
131
137
  | `skillscout list [--all] [-n N]` | Ranked candidates |
132
138
  | `skillscout show <id>` | Flags, conflicts and the full SKILL.md |
133
- | `skillscout approve <id> [--update] [--accept-risk]` | Install and pin |
134
- | `skillscout installed` · `skillscout remove <id>` | Pinned skills · uninstall |
139
+ | `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
+ | `skillscout installed` · `skillscout remove <id>` · `skillscout rollback <id>` | Pinned skills · uninstall · undo the last update |
135
141
  | `skillscout updates` | Diff pinned skills against upstream |
136
142
  | `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 |
137
143
 
138
- 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 300 a run with a token, 40 without. 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.
144
+ 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.
139
145
 
140
146
  ## Limits
141
147
 
142
148
  - The scanner catches known patterns. It can't prove a skill is safe, so read `show` before you approve.
143
149
  - Relevance is keyword matching against your profile, not semantic search. Tune the weights.
150
+ - 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.
144
151
  - Every installed skill's description loads into every agent session. Keep the total to a few dozen.
145
152
 
146
153
  ## Development
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skillscout-cli"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Keep your agent's skills current: weekly discovery, upstream diffs, safety scans, pinned installs"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -7,7 +7,7 @@ import subprocess
7
7
  import sys
8
8
  from pathlib import Path
9
9
 
10
- from . import __doc__ as ABOUT, core, schedule
10
+ from . import __doc__ as ABOUT, core, github, index, schedule
11
11
 
12
12
  EXAMPLE = Path(__file__).resolve().parent / "profile.example.toml"
13
13
  USER_PROFILE = Path.home() / ".skillscout" / "profile.toml"
@@ -25,20 +25,27 @@ def main(argv: list[str] | None = None) -> None:
25
25
  p.add_argument("--profile", type=Path, default=None, help="default: ~/.skillscout/profile.toml, else the example")
26
26
  sub = p.add_subparsers(dest="cmd", required=True)
27
27
  sub.add_parser("init", help="copy the example profile to ~/.skillscout/profile.toml")
28
- d = sub.add_parser("discover", help="search GitHub, scan and score skills, write a report")
29
- d.add_argument("--full", action="store_true", help="ignore caches: fetch every repo and re-scan every skill")
28
+ d = sub.add_parser("discover", help="score this week's shared index against your profile and write a report")
29
+ d.add_argument("--local", action="store_true", help="crawl and scan GitHub on this machine instead of using the index")
30
+ d.add_argument("--full", action="store_true", help="with --local: ignore caches, fetch every repo and re-scan every skill")
30
31
  d.add_argument("--notify", action="store_true", help="desktop notification with the summary (macOS, or Linux with notify-send)")
31
32
  ls = sub.add_parser("list", help="show scored candidates")
32
33
  ls.add_argument("--all", action="store_true", help="include zero-relevance skills")
33
34
  ls.add_argument("-n", type=int, default=30)
34
- sh = sub.add_parser("show", help="print a candidate's flags and full SKILL.md")
35
+ ix = sub.add_parser("index", help="crawl and scan the ecosystem and write the shared index (what the weekly job runs)")
36
+ ix.add_argument("--out", type=Path, default=Path("index.json.gz"))
37
+ ix.add_argument("--full", action="store_true", help="ignore caches: fetch every repo and re-scan every skill")
38
+ sh = sub.add_parser("show", help="fetch a candidate, scan it here, print its flags and full SKILL.md")
35
39
  sh.add_argument("id")
36
- ap = sub.add_parser("approve", help="install one skill into ~/.claude/skills and pin it")
40
+ ap = sub.add_parser("approve", help="install one skill (into ~/.claude/skills unless [install] dir says otherwise) and pin it")
37
41
  ap.add_argument("id")
42
+ ap.add_argument("--to", type=Path, help="install into this skills folder instead, e.g. another agent's")
38
43
  ap.add_argument("--accept-risk", action="store_true", help="install despite high-risk flags")
39
44
  ap.add_argument("--update", action="store_true", help="replace a copy skillscout installed earlier")
40
45
  rm = sub.add_parser("remove", help="uninstall a skill skillscout installed")
41
46
  rm.add_argument("id")
47
+ rb = sub.add_parser("rollback", help="restore the copy the last `approve --update` replaced")
48
+ rb.add_argument("id")
42
49
  sub.add_parser("installed", help="show pinned skills")
43
50
  sub.add_parser("updates", help="diff pinned skills against upstream; applies nothing")
44
51
  sub.add_parser("schedule", help="set up weekly discovery for this OS (you turn it on)")
@@ -49,36 +56,51 @@ def main(argv: list[str] | None = None) -> None:
49
56
  if USER_PROFILE.exists():
50
57
  raise SystemExit(f"{USER_PROFILE} already exists")
51
58
  USER_PROFILE.parent.mkdir(parents=True, exist_ok=True)
52
- USER_PROFILE.write_text(EXAMPLE.read_text())
59
+ USER_PROFILE.write_text(EXAMPLE.read_text(encoding="utf-8"), encoding="utf-8")
53
60
  print(f"wrote {USER_PROFILE}; edit [keywords] to match your work, then run `skillscout discover`")
54
61
  return
55
62
  profile = core.load_profile(a.profile or (USER_PROFILE if USER_PROFILE.exists() else EXAMPLE))
63
+ if folder := getattr(a, "to", None) or profile.get("install", {}).get("dir"):
64
+ paths.skills = Path(folder).expanduser()
56
65
  cands = core._read(paths.candidates, [])
57
66
 
58
67
  if a.cmd == "discover":
59
- cands = core.discover(profile, paths, full=a.full)
68
+ url = profile.get("index", {}).get("url", index.URL)
69
+ if a.local or not url:
70
+ cands = core.discover(profile, paths, full=a.full)
71
+ else:
72
+ try:
73
+ cands = core.discover_from_index(profile, paths, url, profile.get("index", {}).get("verify", False))
74
+ except index.Unavailable as e:
75
+ raise SystemExit(str(e))
60
76
  out, summary = core.report(cands, profile, paths)
61
77
  print(f"{summary}\nreport: {out}")
62
78
  if a.notify:
63
79
  notify(summary)
80
+ elif a.cmd == "index":
81
+ records = core.crawl(profile["search"], paths, full=a.full)
82
+ data = index.build(records, a.out, core.RULES)
83
+ print(f"wrote {a.out}: {len(data['skills'])} skills from {len({r['repo'] for r in records})} repos")
64
84
  elif a.cmd == "list":
65
85
  rows = [c for c in cands if a.all or c["relevance"] > 0][: a.n]
66
86
  for c in rows:
67
87
  tag = "installed" if c["installed"] else ("REC" if core.recommended(c, profile) else "")
68
88
  print(f"{c['score']:.2f} {c['risk'] or '-':6} {tag:9} {c['id']}")
69
89
  elif a.cmd == "show":
70
- c = core.find(cands, a.id)
90
+ c = core.ensure_local(core.find(cands, a.id), paths)
71
91
  print(json.dumps({k: c[k] for k in ("id", "score", "stars", "license", "commit", "risk", "matched", "conflicts")}, indent=2))
72
92
  for f in c["flags"]:
73
93
  print(f" [{f['level']}] {f['reason']} — {f['where']}")
74
94
  src = core.source_dir(c, paths)
75
95
  print(f"\nfiles: {', '.join(sorted(str(x.relative_to(src)) for x in src.rglob('*') if x.is_file()))}\n")
76
- print((src / "SKILL.md").read_text(errors="ignore"))
96
+ print((src / "SKILL.md").read_text(encoding="utf-8", errors="ignore"))
77
97
  elif a.cmd == "approve":
78
98
  c = core.find(cands, a.id)
79
- print(f"installed {c['id']} -> {core.approve(c, paths, a.accept_risk, a.update)}")
99
+ print(f"installed {c['id']} -> {core.approve(c, paths, a.accept_risk, a.update, fetch=github)}")
80
100
  elif a.cmd == "remove":
81
101
  print(f"removed {core.remove(a.id, paths)}")
102
+ elif a.cmd == "rollback":
103
+ print(f"restored the previous {a.id} -> {core.rollback(a.id, paths)}")
82
104
  elif a.cmd == "installed":
83
105
  for cid, e in core._read(paths.lock, {}).items():
84
106
  print(f"{cid} @{e['commit'][:10]} {e['installed_at']}")