@lorekit/cli 1.12.0 → 1.13.0

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/README.md CHANGED
@@ -477,9 +477,11 @@ active deny constraints.
477
477
  | `LOREKIT_TELEMETRY_TOKEN` | bearer token for telemetry export (overrides the baked-in default) |
478
478
  | `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_HEADERS` | override the telemetry OTLP endpoint / headers |
479
479
 
480
- ## What the skill does
480
+ ## What the skills do
481
481
 
482
- The installed `lorekit-memory` skill teaches an agent to:
482
+ `install` scaffolds two skills:
483
+
484
+ The **`lorekit-memory`** skill teaches an agent to:
483
485
 
484
486
  - **Read** scoped lessons at the start of a task, on first navigation into
485
487
  unfamiliar code, and before risky operations (narrow-to-broad scope merge).
@@ -491,11 +493,16 @@ This mirrors the read-on-start / write-on-failure loop of the `aw`
491
493
  autonomous-workflow agent. See the skill's own `SKILL.md` for the full
492
494
  protocol.
493
495
 
494
- The **skill** is model-invoked (the agent chooses to use it). For a
496
+ The **`lorekit-setup`** skill is the authoring counterpart: it teaches an agent
497
+ to wire a self-improvement loop into one of *your own* skills or workflows — the
498
+ two-tier model, the lesson bucket convention, and the entrenchment guards. See
499
+ its `SKILL.md` and `rules/self-improvement-loops.md`.
500
+
501
+ The **skills** are model-invoked (the agent chooses to use them). For a
495
502
  **deterministic** guarantee — lessons injected on every session start, a nudge
496
503
  on every tool failure — use the framework plugins in [`plugins/`](../../plugins/),
497
- which fire the `lorekit hook` engine on host lifecycle events. The skill and
498
- the hooks compose: hooks guarantee the *timing*, the skill supplies the
504
+ which fire the `lorekit hook` engine on host lifecycle events. The skills and
505
+ the hooks compose: hooks guarantee the *timing*, the skills supply the
499
506
  *authoring judgment*.
500
507
 
501
508
  ## Testing & validating across frameworks
package/bin/lorekit.mjs CHANGED
@@ -32,14 +32,14 @@ ${c.bold('Usage')}
32
32
  npx @lorekit/cli <command> [options]
33
33
 
34
34
  ${c.bold('Commands')}
35
- install Scaffold the lorekit-memory skill, wire the LoreKit MCP server,
36
- and install the deterministic hooks (lessons on SessionStart, a
37
- nudge on tool failure + at Stop). Prompts to install for this
38
- project (.claude) or globally for every project (~/.claude);
39
- --project / --global choose non-interactively, --no-hooks skips
40
- the hooks (skill stays model-invoked only).
41
- uninstall Reverse install: remove the lorekit-memory skill, the MCP server
42
- entry, and the lifecycle hooks for the chosen scope. Surgical —
35
+ install Scaffold the lorekit-memory + lorekit-setup skills, wire the
36
+ LoreKit MCP server, and install the deterministic hooks (lessons
37
+ on SessionStart, a nudge on tool failure + at Stop). Prompts to
38
+ install for this project (.claude) or globally for every project
39
+ (~/.claude); --project / --global choose non-interactively,
40
+ --no-hooks skips the hooks (skills stay model-invoked only).
41
+ uninstall Reverse install: remove the lorekit-memory + lorekit-setup skills,
42
+ the MCP server entry, and the lifecycle hooks for the chosen scope. Surgical —
43
43
  other servers, hooks, and settings are left untouched. Prompts
44
44
  project vs global; --project / --global choose non-interactively.
45
45
  doctor Verify the skill install, MCP connectivity, token, and scope.
@@ -131,13 +131,14 @@ Run ${c.cyan('lorekit <command> --help')} for command-specific options.
131
131
  // focused entry instead of the full top-level HELP, so a user only sees the
132
132
  // flags that actually apply to what they're running.
133
133
  const COMMAND_HELP = {
134
- install: `${c.bold('lorekit install')} — scaffold the skill, wire the MCP server, install the hooks
134
+ install: `${c.bold('lorekit install')} — scaffold the skills, wire the MCP server, install the hooks
135
135
 
136
136
  ${c.bold('Usage')}
137
137
  npx @lorekit/cli install [options]
138
138
 
139
- Scaffolds the lorekit-memory skill, adds the LoreKit MCP server, and wires the
140
- deterministic hooks (lessons on SessionStart, a nudge on tool failure + at Stop).
139
+ Scaffolds the lorekit-memory (runtime read/write) and lorekit-setup (loop
140
+ authoring) skills, adds the LoreKit MCP server, and wires the deterministic
141
+ hooks (lessons on SessionStart, a nudge on tool failure + at Stop).
141
142
 
142
143
  ${c.bold('Options')}
143
144
  -d, --dir <path> Target project root (default: current directory)
@@ -159,8 +160,8 @@ ${c.bold('Examples')}
159
160
  ${c.bold('Usage')}
160
161
  npx @lorekit/cli uninstall [options]
161
162
 
162
- Removes the lorekit-memory skill, the MCP server entry, and the lifecycle hooks.
163
- Surgical — other servers, hooks, and settings are left untouched.
163
+ Removes the lorekit-memory and lorekit-setup skills, the MCP server entry, and
164
+ the lifecycle hooks. Surgical — other servers, hooks, and settings are left untouched.
164
165
 
165
166
  ${c.bold('Options')}
166
167
  -d, --dir <path> Target project root (default: current directory)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Install the LoreKit shared-memory skill and run health checks for the LoreKit MCP server.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,6 +46,12 @@ Both jobs run through LoreKit's `memory.*` MCP tools.
46
46
  If those tools are not connected, this skill is a no-op — say so once and
47
47
  continue the task; never block work because memory is unavailable.
48
48
 
49
+ > **Wiring a loop into your own skill?** This skill does the runtime read/write.
50
+ > To set up a *self-improvement loop* for a skill, workflow, or agent — so it
51
+ > reads its own lessons on every run and promotes proven ones into permanent
52
+ > rules — use the **`lorekit-setup`** skill, the authoring counterpart to this
53
+ > one.
54
+
49
55
  > **Modes.** Memory has a controllable backend (`lorekit doctor` shows the
50
56
  > resolved one): `remote` (the hosted LoreKit server — the default), `local`
51
57
  > (markdown files in two tiers — a per-user **home** tier at `~/.lorekit/` and
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: lorekit-setup
3
+ description: >
4
+ Sets up a self-improvement loop for a skill, workflow, or agent using LoreKit,
5
+ so a host gets better across runs by reading its own accumulated lessons at
6
+ the start of every run and hardening the proven ones into permanent rules.
7
+ Designs the two tiers (a fast episodic tier of LoreKit lessons, advisory-only;
8
+ a slow procedural tier that promotes a recurring lesson into a host rule),
9
+ chooses the lesson bucket (tag + key namespace) and scopes, and installs the
10
+ entrenchment guards that stop a learning loop from reinforcing its own
11
+ mistakes. Runtime reading and writing of lessons is the lorekit-memory skill;
12
+ this is the authoring counterpart. Use when giving a host durable cross-run
13
+ memory or wiring a lessons loop. Triggers on "set up memory for my skill",
14
+ "add a self-improvement loop", "give my workflow memory", "make this learn
15
+ from its mistakes", "self-improving memory", "/lorekit-setup".
16
+ user-invocable: true
17
+ argument-hint: '[host-name]'
18
+ license: MIT
19
+ metadata:
20
+ author: mthines
21
+ version: '1.0.0'
22
+ workflow_type: memory-loop-authoring
23
+ tags:
24
+ - lorekit
25
+ - self-improvement
26
+ - memory
27
+ - lessons
28
+ - loop
29
+ - authoring
30
+ - setup
31
+ ---
32
+
33
+ # LoreKit Setup
34
+
35
+ Give a skill, workflow, or agent a **self-improvement loop**: it reads its own
36
+ accumulated lessons at the start of every run and hardens the proven ones into
37
+ permanent rules — so it gets better the more it runs.
38
+
39
+ This is the **authoring** counterpart to `lorekit-memory`. `lorekit-memory` does
40
+ the runtime read/write of individual lessons; `lorekit-setup` wires the durable
41
+ loop that calls those primitives on a host's behalf. Both run on the same
42
+ LoreKit `memory.*` MCP tools.
43
+
44
+ ## The two tiers (in one screen)
45
+
46
+ | Tier | Mechanism | Changes behavior? |
47
+ | ---- | --------- | ----------------- |
48
+ | **Fast (episodic)** | LoreKit lessons in a per-host bucket, read at the start of a run, written on failure | **No** — advisory input only |
49
+ | **Slow (procedural)** | A human-reviewed edit that hardens a recurring lesson into a host rule | **Yes** |
50
+
51
+ A recurrence gate connects them: a lesson that recurs (`seen_count >= 3`) or is
52
+ marked `status=structural` becomes promotion-eligible. Entrenchment guards keep
53
+ the fast tier from reinforcing its own wrong conclusions.
54
+
55
+ ## Set up a loop
56
+
57
+ Follow [rules/self-improvement-loops.md](./rules/self-improvement-loops.md).
58
+ It covers: when to add a loop (and when not to), the bucket convention (tag
59
+ `loop::<host>-lessons` + key namespace), scope selection, the lesson schema, the
60
+ read/write steps, the promotion gate, the entrenchment guards, a wiring
61
+ checklist, and an interactive setup flow.
62
+
63
+ If invoked with a `host-name`, set up the loop for that host; otherwise ask
64
+ which skill / workflow / agent the loop is for, then walk the interactive setup.
65
+
66
+ The loop's runtime tier needs LoreKit's `memory.*` tools connected; if they are
67
+ not, the host's loop is a silent no-op (the slow tier — a normal source edit —
68
+ still works). Designing the loop needs no connection.
@@ -0,0 +1,257 @@
1
+ # Setting up a self-improvement loop
2
+
3
+ Use this when someone wants a skill, workflow, or agent to **get better across
4
+ runs** — "give my skill memory", "add a self-improvement loop", "make this
5
+ learn from its mistakes", "set up lessons for this workflow".
6
+
7
+ Reading and writing individual lessons at runtime is the **`lorekit-memory`**
8
+ skill (its `intake` / `retrospective` rules). This skill is one level up: how to
9
+ wire a **durable loop** into a host so it reads its own accumulated lessons at
10
+ the start of every run and records new ones on failure — safely, without the
11
+ loop entrenching its own mistakes. The loop runs on the same `memory.*` MCP
12
+ tools `lorekit-memory` uses.
13
+
14
+ The design has two tiers connected by a recurrence gate. Both run on LoreKit.
15
+
16
+ ## Contents
17
+
18
+ - [When to add a loop (and when not to)](#when-to-add-a-loop-and-when-not-to)
19
+ - [The two tiers](#the-two-tiers)
20
+ - [Conventions](#conventions)
21
+ - [Read step (start of every run)](#read-step-start-of-every-run)
22
+ - [Write step (on failure / at the end of a run)](#write-step-on-failure--at-the-end-of-a-run)
23
+ - [Promotion (fast → slow)](#promotion-fast--slow)
24
+ - [Entrenchment guards (do not skip these)](#entrenchment-guards-do-not-skip-these)
25
+ - [Wiring checklist](#wiring-checklist)
26
+ - [Interactive setup](#interactive-setup)
27
+
28
+ ---
29
+
30
+ ## When to add a loop (and when not to)
31
+
32
+ Add a loop when **all** of these hold:
33
+
34
+ - The host is an **orchestrator or multi-step pipeline** that can fail in
35
+ recurring, classifiable ways (wrong triage, a missed step, a false-green gate).
36
+ - Its failures are about the **host's own process**, not just the user's product.
37
+ - There is somewhere a proven lesson could eventually **harden into a rule** (the
38
+ slow tier below).
39
+
40
+ Do **not** add a loop to:
41
+
42
+ - **One-shot utilities** with no durable cross-run subject — the bookkeeping
43
+ costs more than it returns.
44
+ - **Adversarial / audit steps** (a red-team pass, a fresh-eyes reviewer) — they
45
+ must not be biased by prior runs; that is the whole point of a fresh pass.
46
+ - **Steps that handle secrets or credentials** — routing them through lesson
47
+ extraction is a leak risk.
48
+
49
+ When unsure, default to **no**. A loop can be added later; unwinding an
50
+ entrenched-bias loop is harder.
51
+
52
+ ---
53
+
54
+ ## The two tiers
55
+
56
+ | Tier | Mechanism | Storage | Changes behavior? | Gate |
57
+ | ---- | --------- | ------- | ----------------- | ---- |
58
+ | **Fast (episodic)** | LoreKit lessons (`memory.*`), read at the start of a run, written on failure / at the end | LoreKit | **No** — advisory input only | none (privacy pre-flight only) |
59
+ | **Slow (procedural)** | A permanent edit to the host's own source / rules | the host skill | **Yes** — a rule / gate / trigger | human review + approval |
60
+
61
+ The fast tier captures a lesson immediately and cheaply. A lesson earns a
62
+ **permanent** change to the host (the slow tier) only once it has **recurred
63
+ across runs** — recurrence is the cheap external signal that the lesson is real,
64
+ not a one-off. This is the episodic → procedural promotion path, with a human
65
+ gate on the slow tier so a single bad run can never rewrite the host.
66
+
67
+ The fast tier is optional: if LoreKit's `memory.*` tools are not connected, the
68
+ loop is a silent no-op (log one line, continue). The slow tier is just editing
69
+ the host and is unaffected.
70
+
71
+ ---
72
+
73
+ ## Conventions
74
+
75
+ Give the loop its own **bucket** so its lessons stay separate from every other
76
+ loop's on the same scopes. A bucket is a tag plus a key namespace:
77
+
78
+ - **Tag:** `loop::<host>-lessons` (e.g. `loop::deploy-lessons`). Reads filter by
79
+ it; writes always carry it.
80
+ - **Key:** `<host>-lessons::<kebab-slug>` (e.g. `deploy-lessons::migration-order`).
81
+ Same `scope` + `key` overwrites in place — the mechanism behind recurrence
82
+ counting.
83
+
84
+ Pick the **scope** per lesson from the standard LoreKit model (`::` is the only
85
+ separator; the `lorekit-memory` skill's scope-resolution reference has the full
86
+ derivation):
87
+
88
+ - **`global`** — universal lessons that should follow the user across every repo.
89
+ - **`repo::{owner}/{repo}`** — lessons specific to this repository.
90
+
91
+ Reserve `branch::` for throwaway notes; a loop normally writes `global` or
92
+ `repo::`.
93
+
94
+ ### The lesson record
95
+
96
+ A loop lesson is **procedural** ("how to do better next time"), not a fact about
97
+ the user. Keep the machine-read metadata in a `meta:` comment at the top of the
98
+ `value` so the prose stays readable:
99
+
100
+ ```markdown
101
+ <!-- meta: seen_count=1 status=active expires=<ISO 8601, ~90 days out> trigger-context="<concrete signal — file glob, task type, tool, error shape>" -->
102
+
103
+ # <one-line lesson title>
104
+
105
+ **What failed:** <concrete observable from the run>
106
+ **Why:** <root cause, if known; "unknown" is allowed>
107
+ **What to do next time:** <prescriptive, actionable, testable instruction>
108
+ **Promotion target:** <the host rule/step this would harden if promoted, or "none">
109
+ ```
110
+
111
+ `trigger-context` must be **concrete** (globs, task types, tool names, error
112
+ shapes) — never "when it feels relevant" — so the read step can match it
113
+ mechanically. `seen_count`, `status`, and `expires` drive recurrence, promotion,
114
+ and decay.
115
+
116
+ ---
117
+
118
+ ## Read step (start of every run)
119
+
120
+ Read narrow-to-broad, filtered by the bucket tag, and merge:
121
+
122
+ ```text
123
+ memory.list { scope: "repo::{owner}/{repo}", tags: ["loop::<host>-lessons"], limit: 50 } # skips silently if memory.* not connected
124
+ memory.list { scope: "global", tags: ["loop::<host>-lessons"], limit: 50 }
125
+ # when the run names a subsystem / error, add a search:
126
+ memory.search { q: "<keywords>", scopes: ["repo::{owner}/*", "global"], limit: 10 }
127
+ ```
128
+
129
+ Then:
130
+
131
+ 1. Match each lesson's `trigger-context` against the current run. Consider only
132
+ matches. **Skip any lesson whose `expires` is in the past** — treat it as
133
+ stale.
134
+ 2. Apply each matching *"What to do next time"* as a **consideration**, not a
135
+ command — it biases the run unless it conflicts with the user's stated intent
136
+ or a task-specific constraint. On conflict, the user's intent wins; surface it.
137
+ 3. On a `repo::` vs `global` collision, the `repo::` lesson wins (closer scope).
138
+
139
+ No consolidation pass is needed — LoreKit owns storage and deduplicates on write.
140
+
141
+ ---
142
+
143
+ ## Write step (on failure / at the end of a run)
144
+
145
+ Trigger on friction: a stuck loop, a repeated failure, a gate that should have
146
+ caught something, a near-miss, a guess that paid off. Not on smooth successes.
147
+
148
+ 1. **Classify the scope.** Trigger references a concrete repo path / repo-specific
149
+ package or term → `repo::{owner}/{repo}`. A glob, framework, tool, or task
150
+ type with no repo binding → `global`. When ambiguous, default to `global`.
151
+ 2. **Deduplicate**, so a recurrence updates in place instead of piling up:
152
+
153
+ ```text
154
+ memory.search { q: "<key words of the lesson>", scopes: ["repo::{owner}/{repo}", "global"], limit: 10 }
155
+ ```
156
+
157
+ 3. **Write** to the classified scope:
158
+
159
+ ```text
160
+ memory.write {
161
+ scope: "<global | repo::{owner}/{repo}>",
162
+ key: "<host>-lessons::<slug>",
163
+ value: "<the lesson body above>",
164
+ tags: ["loop::<host>-lessons", "source::<trigger>"],
165
+ trigger: "<stuck-loop | command-failure | gotcha | near-miss | assumption-wrong | paid-off | manual>"
166
+ }
167
+ ```
168
+
169
+ Same `scope` + `key` overwrites in place. **A recurrence resolves to an UPDATE
170
+ that increments `seen_count` by 1 and refreshes `expires`** — that is what makes
171
+ recurrence countable and drives promotion. If a lesson you applied at the start
172
+ of the run worked (the failure did not recur), still write the UPDATE:
173
+ successful application is recurrence evidence.
174
+
175
+ The privacy pre-flight is never skipped, autonomous or not: a candidate lesson
176
+ containing a secret, token, credential, or PII is **dropped, not written**. The
177
+ bar is stricter for `repo::` writes — a repo scope is team-visible.
178
+
179
+ ---
180
+
181
+ ## Promotion (fast → slow)
182
+
183
+ After a read or write, a lesson is **promotion-eligible** when either:
184
+
185
+ - `seen_count >= 3` — the same failure recurred across at least three runs, or
186
+ - it is tagged `status=structural` because it reflects a design gap, not a
187
+ one-off.
188
+
189
+ For an eligible lesson, **surface a one-line suggestion — never act silently**:
190
+
191
+ > Lesson "<title>" recurred N times. Promote it to a permanent guard in
192
+ > `<host>`? (edit the host's source / rules behind your normal review.)
193
+
194
+ The promotion target follows the lesson's scope: a `global` lesson hardens the
195
+ **host's own source** (every user of the host benefits); a `repo::` lesson
196
+ hardens the **repo's own rules / docs** (every teammate in that repo benefits).
197
+ Promotion is a normal, human-reviewed edit — LoreKit does not apply it. After a
198
+ successful promotion, write an UPDATE setting `status=promoted` so the lesson
199
+ stops re-suggesting and stands as an audit trail of why the rule exists.
200
+
201
+ ---
202
+
203
+ ## Entrenchment guards (do not skip these)
204
+
205
+ The dominant risk of any reflective-memory loop is **self-reinforcing error**:
206
+ the host wrongly concludes "approach X always fails", stores it, avoids X
207
+ forever, and never gathers the evidence to overturn the false belief. These
208
+ guards are what make the loop safe:
209
+
210
+ 1. **Lessons are advisory, never auto-applied to behavior.** A lesson biases a
211
+ run; it can never silently disable a gate, skip a step, or change a limit. The
212
+ only path from a lesson to changed behavior is the human-reviewed slow tier.
213
+ 2. **Recurrence gates promotion, not a single run** (`seen_count >= 3`, or an
214
+ explicit `status=structural` marker in the lesson's `meta:` comment).
215
+ 3. **Every lesson expires** (default ~90 days from last sighting; the read step
216
+ ignores expired lessons, so stale beliefs decay instead of entrenching).
217
+ 4. **Contradiction is surfaced, not silently overwritten** — the dedup search
218
+ finds the prior lesson; a genuine reversal is a reviewed decision.
219
+ 5. **The privacy pre-flight is never bypassed** — secrets / PII are dropped, not
220
+ stored.
221
+
222
+ ---
223
+
224
+ ## Wiring checklist
225
+
226
+ To add a loop to a host called `<host>`:
227
+
228
+ - [ ] Pick the bucket: tag `loop::<host>-lessons`, key `<host>-lessons::<slug>`.
229
+ - [ ] Add the **read step** at the start of the host's run (narrow-to-broad
230
+ `memory.list` filtered by the tag; apply matches as considerations; skip
231
+ expired).
232
+ - [ ] Add the **write step** at the host's existing failure / end-of-run points
233
+ (classify scope, `memory.search` to dedup, `memory.write`). No new
234
+ reflection stage — hook the points the host already detects.
235
+ - [ ] Add the **promotion suggestion** when a read/written lesson hits
236
+ `seen_count >= 3` or `status=structural`.
237
+ - [ ] State the **entrenchment guards** so a future maintainer does not "optimize
238
+ them away".
239
+ - [ ] Confirm the loop **degrades silently** when `memory.*` is not connected.
240
+
241
+ ---
242
+
243
+ ## Interactive setup
244
+
245
+ When a user asks to set up self-improving memory for a specific host, walk them
246
+ through it rather than dumping the whole recipe:
247
+
248
+ 1. **Confirm it should have a loop** — run the "when to add a loop" test above.
249
+ If the host is a one-shot utility, an adversarial pass, or a secret-handler,
250
+ say so and stop.
251
+ 2. **Name the bucket** — `loop::<host>-lessons` from the host's name.
252
+ 3. **Locate the read and write points** — the start of a run, and the failure /
253
+ end-of-run points the host already has. Show the two `memory.*` snippets with
254
+ the bucket filled in.
255
+ 4. **Write the guards down** in the host's own docs, so they survive future edits.
256
+ 5. **Do a dry run** — trigger a failure, confirm a lesson is written to the right
257
+ scope with the right tag, then confirm the next run reads it back.
package/src/config.mjs CHANGED
@@ -6,8 +6,17 @@ import { fileURLToPath } from 'node:url';
6
6
 
7
7
  // packages/cli/ — the installable package root (this file lives in src/).
8
8
  export const PKG_ROOT = fileURLToPath(new URL('../', import.meta.url));
9
- export const SKILL_SOURCE = path.join(PKG_ROOT, 'skill', 'lorekit-memory');
10
9
  export const SKILL_NAME = 'lorekit-memory';
10
+ export const SKILL_SOURCE = path.join(PKG_ROOT, 'skill', SKILL_NAME);
11
+
12
+ // Every skill the CLI ships. `lorekit-memory` is the operational read/write
13
+ // loop (kept first as the primary — `SKILL_NAME`/`SKILL_SOURCE` above alias it
14
+ // for back-compat); `lorekit-setup` is the authoring skill that wires a
15
+ // self-improvement loop into a host. install/uninstall/doctor iterate this list.
16
+ export const SKILLS = ['lorekit-memory', 'lorekit-setup'].map((name) => ({
17
+ name,
18
+ source: path.join(PKG_ROOT, 'skill', name),
19
+ }));
11
20
 
12
21
  export function resolveProjectRoot(dir) {
13
22
  return path.resolve(dir || process.cwd());
@@ -62,9 +71,9 @@ export function mcpConfigPath(root, scope = 'project') {
62
71
  // Where the skill is scaffolded for a given scope:
63
72
  // project → <root>/.claude/skills/… (this repo only)
64
73
  // global → ~/.claude/skills/… (personal skills, all projects)
65
- export function skillInstallDir(root, scope = 'project') {
74
+ export function skillInstallDir(root, scope = 'project', name = SKILL_NAME) {
66
75
  const base = scope === 'global' ? homeDir() : root;
67
- return path.join(base, '.claude', 'skills', SKILL_NAME);
76
+ return path.join(base, '.claude', 'skills', name);
68
77
  }
69
78
 
70
79
  // Claude Code settings file that holds the hooks for a given scope:
@@ -231,10 +240,10 @@ export function readLorekitServer(root) {
231
240
  // server / hook / setting intact, so uninstalling never damages a shared
232
241
  // ~/.claude.json or settings.json.
233
242
 
234
- // Delete the scaffolded skill directory. We own the whole
235
- // .claude/skills/lorekit-memory tree, so a recursive remove is safe.
236
- export function removeSkill(root, scope = 'project') {
237
- const dest = skillInstallDir(root, scope);
243
+ // Delete a scaffolded skill directory. We own the whole
244
+ // .claude/skills/<name> tree for each skill we ship, so a recursive remove is safe.
245
+ export function removeSkill(root, scope = 'project', name = SKILL_NAME) {
246
+ const dest = skillInstallDir(root, scope, name);
238
247
  const removed = fs.existsSync(dest);
239
248
  if (removed) fs.rmSync(dest, { recursive: true, force: true });
240
249
  return { dest, removed };
package/src/doctor.mjs CHANGED
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import process from 'node:process';
6
6
  import { execFileSync } from 'node:child_process';
7
7
  import {
8
- SKILL_NAME,
8
+ SKILLS,
9
9
  resolveProjectRoot,
10
10
  skillInstallDir,
11
11
  readLorekitServer,
@@ -42,18 +42,24 @@ export async function doctor(args) {
42
42
  `v${process.versions.node}${major < 18 ? ' — need v18+ for fetch' : ''}`,
43
43
  );
44
44
 
45
- // 2. Skill installed — check BOTH the project and the global (~/.claude)
46
- // locations. `lorekit install --global` writes the skill under home, not the
47
- // repo, so a project-only check reports a healthy global install as "not
48
- // found" (exactly the false FAIL a --global setup would hit).
49
- const skillMd = [skillInstallDir(root, 'project'), skillInstallDir(root, 'global')]
50
- .map((dir) => path.join(dir, 'SKILL.md'))
51
- .find((p) => fs.existsSync(p));
52
- if (skillMd) {
53
- const rel = path.relative(root, skillMd);
54
- record('pass', `skill ${SKILL_NAME}`, rel && !rel.startsWith('..') ? rel : prettyPath(skillMd));
55
- } else {
56
- record('fail', `skill ${SKILL_NAME}`, 'not found — run `lorekit install`');
45
+ // 2. Skills installed — check every skill the CLI ships, in BOTH the project
46
+ // and the global (~/.claude) locations. `lorekit install --global` writes the
47
+ // skill under home, not the repo, so a project-only check reports a healthy
48
+ // global install as "not found" (exactly the false FAIL a --global setup would
49
+ // hit).
50
+ for (const skill of SKILLS) {
51
+ const skillMd = [
52
+ skillInstallDir(root, 'project', skill.name),
53
+ skillInstallDir(root, 'global', skill.name),
54
+ ]
55
+ .map((dir) => path.join(dir, 'SKILL.md'))
56
+ .find((p) => fs.existsSync(p));
57
+ if (skillMd) {
58
+ const rel = path.relative(root, skillMd);
59
+ record('pass', `skill ${skill.name}`, rel && !rel.startsWith('..') ? rel : prettyPath(skillMd));
60
+ } else {
61
+ record('fail', `skill ${skill.name}`, 'not found — run `lorekit install`');
62
+ }
57
63
  }
58
64
 
59
65
  // 3. Resolved control model — which mode, and who decided it.
package/src/install.mjs CHANGED
@@ -1,11 +1,10 @@
1
- // `lorekit install` — scaffold the skill and wire the MCP server.
1
+ // `lorekit install` — scaffold the skills and wire the MCP server.
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import readline from 'node:readline';
5
5
  import process from 'node:process';
6
6
  import {
7
- SKILL_SOURCE,
8
- SKILL_NAME,
7
+ SKILLS,
9
8
  resolveProjectRoot,
10
9
  skillInstallDir,
11
10
  copyDir,
@@ -73,10 +72,13 @@ export async function install(args) {
73
72
  token = token || null;
74
73
  }
75
74
 
76
- // 3. Install the skill files.
77
- const dest = skillInstallDir(root, scope);
78
- const skillExisted = fs.existsSync(path.join(dest, 'SKILL.md'));
79
- const written = copyDir(SKILL_SOURCE, dest, { force: Boolean(args.force) });
75
+ // 3. Install the skill files — every skill the CLI ships.
76
+ const skillResults = SKILLS.map((skill) => {
77
+ const dest = skillInstallDir(root, scope, skill.name);
78
+ const existed = fs.existsSync(path.join(dest, 'SKILL.md'));
79
+ const written = copyDir(skill.source, dest, { force: Boolean(args.force) });
80
+ return { name: skill.name, dest, existed, written };
81
+ });
80
82
 
81
83
  // 4. Wire the MCP config for the chosen scope.
82
84
  const remoteUrl = buildRemoteUrl(endpoint, token);
@@ -100,12 +102,14 @@ export async function install(args) {
100
102
 
101
103
  // 5. Report.
102
104
  heading('Done');
103
- const skillState = !skillExisted
104
- ? 'installed'
105
- : written > 0
106
- ? `updated (${written} file(s) written)`
107
- : 'unchanged pass --force to overwrite';
108
- status(skillExisted && written === 0 ? 'info' : 'pass', `skill ${SKILL_NAME}`, `${skillState} → ${display(dest)}`);
105
+ for (const s of skillResults) {
106
+ const skillState = !s.existed
107
+ ? 'installed'
108
+ : s.written > 0
109
+ ? `updated (${s.written} file(s) written)`
110
+ : 'unchanged pass --force to overwrite';
111
+ status(s.existed && s.written === 0 ? 'info' : 'pass', `skill ${s.name}`, `${skillState} → ${display(s.dest)}`);
112
+ }
109
113
  status('pass', mcpLabel, `${existed ? 'updated' : 'created'} lorekit server → ${display(file)}`);
110
114
 
111
115
  if (!wireHooks) {
Binary file
@@ -316,7 +316,7 @@ class TwoTierStore {
316
316
  const projRes = this.projectActive()
317
317
  ? await this.project.search({ q, scopes, tags })
318
318
  : { entries: [] };
319
- const merged = mergeByKey(projRes.entries, homeRes.entries, (e) => `${e.scope}${e.key}`);
319
+ const merged = mergeByKey(projRes.entries, homeRes.entries, (e) => `${e.scope}\x00${e.key}`);
320
320
  return { ok: true, entries: merged };
321
321
  }
322
322
 
package/src/uninstall.mjs CHANGED
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import readline from 'node:readline';
6
6
  import process from 'node:process';
7
7
  import {
8
- SKILL_NAME,
8
+ SKILLS,
9
9
  resolveProjectRoot,
10
10
  removeSkill,
11
11
  removeMcpServer,
@@ -51,7 +51,10 @@ export async function uninstall(args) {
51
51
  // install — never clobber what we can't understand), so we catch and report it
52
52
  // cleanly, leaving that file byte-for-byte untouched. Atomic writes
53
53
  // (writeFileAtomic) guarantee a config can't be half-written even on a crash.
54
- const skill = attempt(() => removeSkill(root, scope));
54
+ const skillSteps = SKILLS.map((skill) => ({
55
+ name: skill.name,
56
+ step: attempt(() => removeSkill(root, scope, skill.name)),
57
+ }));
55
58
  const mcp = attempt(() => removeMcpServer(root, scope));
56
59
  const hooks = attempt(() => removeClaudeHooks(root, scope));
57
60
 
@@ -61,10 +64,12 @@ export async function uninstall(args) {
61
64
  const mcpLabel = scope === 'global' ? '~/.claude.json' : '.mcp.json';
62
65
 
63
66
  heading('Done');
64
- report(skill, `skill ${SKILL_NAME}`, {
65
- done: (r) => `removed ${display(r.dest)}`,
66
- noop: 'not installed nothing to remove',
67
- });
67
+ for (const { name, step } of skillSteps) {
68
+ report(step, `skill ${name}`, {
69
+ done: (r) => `removed ${display(r.dest)}`,
70
+ noop: 'not installed — nothing to remove',
71
+ });
72
+ }
68
73
  report(mcp, mcpLabel, {
69
74
  done: (r) => `lorekit server removed → ${display(r.file)}`,
70
75
  noop: 'no lorekit server entry — nothing to remove',
@@ -74,8 +79,10 @@ export async function uninstall(args) {
74
79
  noop: 'no lorekit hooks — nothing to remove',
75
80
  });
76
81
 
77
- const failed = [skill, mcp, hooks].some((s) => !s.ok);
78
- const any = (skill.result?.removed || mcp.result?.removed || hooks.result?.removed) && true;
82
+ const skillStepList = skillSteps.map((s) => s.step);
83
+ const failed = [...skillStepList, mcp, hooks].some((s) => !s.ok);
84
+ const any =
85
+ (skillStepList.some((s) => s.result?.removed) || mcp.result?.removed || hooks.result?.removed) && true;
79
86
 
80
87
  if (failed) {
81
88
  log(`\n ${c.dim('Some items could not be removed and were left untouched — see above.')}`);