@ericrisco/rsc 0.1.26 → 0.1.28

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.
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.26",
2
+ "version": "0.1.28",
3
3
  "counts": {
4
4
  "skills": 231
5
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,22 +14,37 @@ const CLI_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
14
14
  // materialized at — the single, target-agnostic source of truth for "installed
15
15
  // skills version" (read by the SessionStart update check too).
16
16
  const versionFile = (cwd) => join(cwd, '.rsc', '.version');
17
- function readBaseVersion(cwd) {
18
- try { return readFileSync(versionFile(cwd), 'utf8').trim(); } catch { return undefined; }
17
+
18
+ // Per-skill base version. A single global `.rsc/.version` cannot represent a
19
+ // partially-refreshed base set, which broke multi-target sync: the first target's pass
20
+ // bumped `.rsc/.version`, so later targets saw "current" and skipped refreshing their
21
+ // exclusive skills' bases. Tracking the version each base was materialized at makes the
22
+ // refresh decision per skill, independent of target ordering. (Absent file → every base
23
+ // is treated as stale and refreshed once, which self-heals installs from before this.)
24
+ const baseVersionsFile = (cwd) => join(cwd, '.rsc', '.base-versions.json');
25
+ function readBaseVersions(cwd) {
26
+ try { return JSON.parse(readFileSync(baseVersionsFile(cwd), 'utf8')); } catch { return {}; }
27
+ }
28
+ function writeBaseVersions(cwd, versions) {
29
+ mkdirSync(dirname(baseVersionsFile(cwd)), { recursive: true });
30
+ writeFileSync(baseVersionsFile(cwd), JSON.stringify(versions, null, 2) + '\n');
19
31
  }
20
32
 
21
- // Materialize the real skill files into the project-local base. Normally copied
22
- // once and reused; when `refresh` is set (a different CLI version than the base was
23
- // materialized at) the base is re-copied so a reinstall actually updates content.
24
- // Skills are read-only catalog (user customization lives in 02-DOCS/CLAUDE.md), so
25
- // overwriting on a version change is safe.
26
- function ensureBase(id, cwd, refresh) {
33
+ // Materialize the real skill files into the project-local base. Copied once and reused;
34
+ // when the recorded base version for THIS skill differs from the CLI version, the base is
35
+ // re-copied so a reinstall/sync actually updates content. Tracked per skill (see
36
+ // baseVersionsFile) so a multi-target sync refreshes every target's bases, not just the
37
+ // first target's. Skills are read-only catalog (user customization lives in 02-DOCS), so
38
+ // overwriting on a version change is safe. Mutates `baseVersions` with the new mark.
39
+ function ensureBase(id, cwd, baseVersions) {
27
40
  const dest = baseDir(id, cwd);
28
- if (refresh && existsSync(dest)) rmSync(dest, { recursive: true, force: true });
41
+ const stale = baseVersions[id] !== CLI_VERSION;
42
+ if (stale && existsSync(dest)) rmSync(dest, { recursive: true, force: true });
29
43
  if (!existsSync(dest)) {
30
44
  mkdirSync(dirname(dest), { recursive: true });
31
45
  cpSync(join(ROOT, 'skills', id), dest, { recursive: true });
32
46
  }
47
+ baseVersions[id] = CLI_VERSION;
33
48
  return dest;
34
49
  }
35
50
 
@@ -46,7 +61,7 @@ function generatedHookFiles({ target, cwd }) {
46
61
  function managedPathsForInstall({ skillIds, target, home, cwd }) {
47
62
  const paths = targetPaths(target, home, cwd);
48
63
  const plan = planInstall({ skillIds, target, home, cwd });
49
- const out = [paths.stateFile, versionFile(cwd)];
64
+ const out = [paths.stateFile, versionFile(cwd), baseVersionsFile(cwd)];
50
65
  for (const step of plan) {
51
66
  if (step.kind === 'skill') {
52
67
  out.push(step.to, baseDir(step.id, cwd));
@@ -64,18 +79,21 @@ export async function applyInstall({ skillIds, target, home, cwd = process.cwd()
64
79
  if (dryRun) return { dryRun: true, skills: skillIds, paths: managedPaths };
65
80
  const state = readState(paths.stateFile);
66
81
  const backup = createBackup({ cwd, operation, target, paths: managedPaths, cliVersion: CLI_VERSION });
67
- // Refresh bases when installing a different version than they were materialized at
68
- // (or a pre-versioning install where the marker is absent). Same version no-op.
69
- const refresh = readBaseVersion(cwd) !== CLI_VERSION;
82
+ // Decide base refresh per skill (see baseVersionsFile): a base is re-materialized when
83
+ // its recorded version differs from the CLI version. Robust to multi-target installs/
84
+ // syncs a single global marker would be bumped by the first target and make later
85
+ // targets skip refreshing their exclusive skills' bases.
86
+ const baseVersions = readBaseVersions(cwd);
70
87
  for (const step of plan) {
71
88
  if (step.kind === 'skill') {
72
- const base = ensureBase(step.id, cwd, refresh);
89
+ const base = ensureBase(step.id, cwd, baseVersions);
73
90
  const files = await writeSkill(target, step.id, base, step.to);
74
91
  state.skills[step.id] = { files, base };
75
92
  } else if (step.kind === 'hook') {
76
- await wireHook(target, paths, join(ensureBase('suggest', cwd, refresh), 'SKILL.md'));
93
+ await wireHook(target, paths, join(ensureBase('suggest', cwd, baseVersions), 'SKILL.md'));
77
94
  }
78
95
  }
96
+ writeBaseVersions(cwd, baseVersions);
79
97
  state.version = CLI_VERSION;
80
98
  writeState(paths.stateFile, state);
81
99
  mkdirSync(dirname(versionFile(cwd)), { recursive: true });
package/scripts/rsc.js CHANGED
@@ -187,6 +187,26 @@ async function main() {
187
187
  for (const o of toOutcomes(ids)) say(`${o.id}\t${o.label}`);
188
188
  return;
189
189
  }
190
+ case 'catalog': {
191
+ // Full catalog dump for SEMANTIC in-agent discovery: every skill as
192
+ // `id <installed|available> short description`, unranked. `consult` ranks
193
+ // lexically and returns nothing for natural-language / Catalan intent; `catalog`
194
+ // hands the agent the whole candidate set so the MODEL picks the best-fit missing
195
+ // skill by meaning. `--available` drops what's already installed for this target.
196
+ const m = loadManifest();
197
+ const installed = new Set(listInstalled({ target }));
198
+ const onlyAvailable = argv.includes('--available');
199
+ const short = (d) => {
200
+ const s = String(d || '').split('. ')[0].replace(/\s+/g, ' ').trim();
201
+ return s.length > 160 ? `${s.slice(0, 159)}…` : s;
202
+ };
203
+ for (const sk of [...m.skills].sort((a, b) => a.id.localeCompare(b.id))) {
204
+ const state = installed.has(sk.id) ? 'installed' : 'available';
205
+ if (onlyAvailable && state === 'installed') continue;
206
+ say(`${sk.id}\t${state}\t${short(sk.description)}`);
207
+ }
208
+ return;
209
+ }
190
210
  case 'audit': {
191
211
  const report = audit();
192
212
  const written = writeAuditReport(report);
@@ -56,8 +56,8 @@ When the current task would clearly benefit from an rsc skill that is **not inst
56
56
  Rules:
57
57
 
58
58
  - Installing changes the user's environment — always confirm first.
59
- - To know what exists, run `npx @ericrisco/rsc consult "<the task>"` instead of guessing.
60
- - For a project-level view, prefer `.rsc/skill-registry.json` when present; if it is missing or stale, suggest `npx @ericrisco/rsc registry refresh`. This is a cheap index, not a reason to load every skill.
59
+ - To know what exists, run `npx @ericrisco/rsc catalog --available` (every NOT-installed skill as `id available short description`) and **pick the best fit yourself, by meaning** — see the detector below. Don't guess from memory.
60
+ - `npx @ericrisco/rsc consult "<task>"` is only a cheap **lexical** hint: it keyword-matches and silently returns nothing for natural-language or non-English/Catalan intent (e.g. it finds no email skill for "mandar emails"). Never let it be the decider; if it's empty, scan `catalog` and judge semantically.
61
61
  - Never recommend something already installed (`npx @ericrisco/rsc list`).
62
62
  - One suggestion at a time. Don't interrupt the flow for nice-to-haves.
63
63
 
@@ -78,24 +78,29 @@ This is broader than "start a project". It includes mid-conversation requests su
78
78
  - Knowledge and research: "documenta cómo funciona esto", "crea una wiki", "procesa este inbox", "research competitors", "turn this into SOPs".
79
79
  - Other languages: any equivalent phrasing. Match the user's intent, not exact words.
80
80
 
81
- When a capability intent appears:
82
-
83
- 1. Run `npx @ericrisco/rsc list` to know what is already installed.
84
- 2. Run `npx @ericrisco/rsc consult "<the user's exact intent>"`.
85
- 3. Read the ordered result as the install queue, but pick only the **first missing**
86
- skill that is useful right now. Skip `suggest` and any installed skill.
81
+ When a capability intent appears, **you** make the match semantically — do not delegate the decision to a keyword ranker:
82
+
83
+ 1. Run `npx @ericrisco/rsc catalog --available` to get every NOT-installed skill as
84
+ `id available short description`. (It already excludes what's installed.)
85
+ 2. **Read the catalog and pick the single best-fit skill by MEANING** — match the user's
86
+ intent to a skill's *purpose*, semantically, in any language (CA/ES/EN/…), the way you'd
87
+ match a request to a teammate's expertise. Judge meaning, not shared keywords:
88
+ "mandar emails de bienvenida" → `email-connector` even though no words overlap its tags;
89
+ "login con Google" → an auth/security skill, not `flutter`; "transcripció de veu" → a
90
+ speech/audio skill *if one exists*.
91
+ 3. If nothing in the catalog genuinely fits, **say so and move on** — don't force a weak
92
+ match, and don't propose a tangential skill just to have an answer. (`npx @ericrisco/rsc
93
+ consult "<intent>"` is available as a lexical hint, but it misses natural-language and
94
+ Catalan intent and is silent for many real needs — never read its silence as "no skill
95
+ exists." The catalog + your judgment is the source of truth.)
87
96
  4. Ask before installing: "Para esto instalaría `<id>`, que aún no tienes. ¿La instalo? (sí/no)".
88
- 5. On yes, run `npx @ericrisco/rsc add <id>` and then continue the original request.
89
-
90
- Example: if the user says "quiero montar una pagina web para vender cursos online",
91
- do not just start building. Check the installed list, consult that exact phrase, and
92
- recommend the first missing skill from the returned queue. In a base install, `init`
93
- and `harness` may already exist, so the first missing skill is usually `nextjs`. In a
94
- bare install, `init` may be first. Install one skill at a time.
97
+ 5. On yes, run `npx @ericrisco/rsc add <id>` and continue the original request. One at a time.
95
98
 
96
- Example: if the user says "hazme una secuencia de cold emails para vender mi SaaS",
97
- consult that exact phrase and recommend the first missing email/marketing skill rather
98
- than writing generic copy with no specialist loaded.
99
+ Example: "quiero montar una pagina web para vender cursos online" scan the catalog and
100
+ pick the web stack skill (usually `nextjs`) by meaning; don't start building before offering it.
101
+ Example: "hazme una secuencia de cold emails para vender mi SaaS" → pick the email/outreach
102
+ skill (`cold-outreach` / `email-connector`) because it *means* email outreach — not because
103
+ the keywords happen to match (they often won't).
99
104
 
100
105
  ## Onboarding gate (first contact)
101
106
 
@@ -62,17 +62,17 @@ capability:
62
62
  - scenario: "Mid-conversation, after base install, the user says 'quiero montar una pagina web para vender cursos online'. `init`, `harness`, `orient`, and `suggest` are already installed; `nextjs` is not. Show how rsc-suggest behaves."
63
63
  must_include:
64
64
  - "Treats the message as in-agent capability intent, not as a CLI-only consult task"
65
- - "Runs `npx @ericrisco/rsc list` before recommending so installed `init`/`harness` are skipped"
66
- - "Runs `npx @ericrisco/rsc consult \"quiero montar una pagina web para vender cursos online\"` or equivalent exact-intent consult"
67
- - "Names `nextjs` as the first missing useful skill for the website work"
65
+ - "Runs `npx @ericrisco/rsc catalog --available` to see the not-installed skills (already excludes installed `init`/`harness`)"
66
+ - "Picks the best-fit skill by MEANING from the catalog, not by relying on lexical `consult` ranking"
67
+ - "Names `nextjs` as the missing useful skill for the website work"
68
68
  - "Asks one short confirmation before installing and does not auto-install"
69
69
  - "On confirmation, runs `npx @ericrisco/rsc add nextjs`, then resumes the original website task"
70
70
 
71
71
  - scenario: "Mid-conversation, the user says 'Automatiza el flujo de leads desde el formulario hasta mi CRM'. The base skills are installed, but `automation-flows` and CRM/connector skills are not. Show how rsc-suggest behaves."
72
72
  must_include:
73
73
  - "Treats the message as in-agent capability intent for automation/integration, not only as a coding task"
74
- - "Runs `npx @ericrisco/rsc list` before recommending"
75
- - "Runs `npx @ericrisco/rsc consult \"Automatiza el flujo de leads desde el formulario hasta mi CRM\"` or equivalent exact-intent consult"
76
- - "Names the first missing useful automation/connector skill from the consult results"
74
+ - "Runs `npx @ericrisco/rsc catalog --available` to see the not-installed skills"
75
+ - "Picks the best-fit missing automation/connector skill by MEANING (not by lexical `consult` ranking, which may return nothing)"
76
+ - "Names that one missing automation/connector skill"
77
77
  - "Asks one short confirmation before installing and does not auto-install"
78
78
  - "On confirmation, runs `npx @ericrisco/rsc add <id>` for that one missing skill, then resumes the automation task"