@lorekit/cli 1.35.0 → 1.37.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 +42 -0
- package/bin/lorekit.mjs +18 -3
- package/package.json +1 -1
- package/src/config.mjs +134 -9
- package/src/install.mjs +93 -9
- package/src/mcp-server.mjs +120 -0
- package/src/store/remote.mjs +34 -2
- package/src/uninstall.mjs +23 -2
package/README.md
CHANGED
|
@@ -74,6 +74,38 @@ lorekit install \
|
|
|
74
74
|
lorekit install --global # set it up for every project
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
#### Claude Code on the web (`--mcp-json`)
|
|
78
|
+
|
|
79
|
+
Claude Code on the web clones the repo fresh into an ephemeral container, so the
|
|
80
|
+
only MCP config it can see is a **committed, repo-root `.mcp.json`** — a global
|
|
81
|
+
`~/.claude.json` lives on your machine and never travels there. `--mcp-json`
|
|
82
|
+
writes exactly that file, in a **committable** form: it authenticates via a
|
|
83
|
+
`${LOREKIT_TOKEN}` reference in an `mcp-remote --header` rather than embedding
|
|
84
|
+
the token, so there is no secret in the file.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
lorekit install --mcp-json --yes # web-ready project .mcp.json
|
|
88
|
+
lorekit install --global --mcp-json --yes # + the machine-wide CLI, skills, hooks
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Set **`LOREKIT_TOKEN`** as an environment secret in the web UI; the value is
|
|
92
|
+
expanded before `mcp-remote` is spawned. `--mcp-json` always writes the
|
|
93
|
+
**project-root** file regardless of `--project` / `--global`, and it composes
|
|
94
|
+
with either — pair it with `--global` to get the local CLI, skills, and hooks in
|
|
95
|
+
`~/.claude` **and** the committable web config in one command. On a `--project`
|
|
96
|
+
install it takes over `.mcp.json` with the committable form instead of the
|
|
97
|
+
embedded-token one.
|
|
98
|
+
|
|
99
|
+
**You have to commit the file, and `.mcp.json` is usually git-ignored** (the
|
|
100
|
+
default install embeds a token, so LoreKit's own `.gitignore` — and many
|
|
101
|
+
projects — ignore it). Un-ignore it before committing (drop the `.mcp.json` line
|
|
102
|
+
from `.gitignore`, negate it with `!.mcp.json`, or `git add -f .mcp.json` once);
|
|
103
|
+
`install --mcp-json` **warns when the file it wrote is still git-ignored**, so a
|
|
104
|
+
fresh web clone silently missing the config is not a mystery. Once `.mcp.json` is
|
|
105
|
+
tracked, only run `install --mcp-json` in that repo — a plain `install --project`
|
|
106
|
+
would embed a live token in the now-committed file. See the
|
|
107
|
+
[Claude Code on the web guide](https://lorekit.io/docs/claude-code-web).
|
|
108
|
+
|
|
77
109
|
In a TTY it prompts for the scope (and for `--endpoint` / `--token` if missing).
|
|
78
110
|
Flags: `--project` / `--global` pick the scope non-interactively; `--yes` runs
|
|
79
111
|
non-interactively (endpoint required via flag/env; scope defaults to project);
|
|
@@ -754,6 +786,7 @@ also returns their headroom against the plan's memory cap.
|
|
|
754
786
|
| `-y, --yes` | Non-interactive / apply; never prompt |
|
|
755
787
|
| `--hooks <mode>` | Lifecycle hooks to wire: `all` / `read-only` / `none` (`install`; `none` removes any already wired) |
|
|
756
788
|
| `--no-hooks` | Skip wiring the lifecycle hooks; skills + MCP only. Leaves already-wired hooks alone (`install`) |
|
|
789
|
+
| `--mcp-json` | Also write a committable project `.mcp.json` (auth via `${LOREKIT_TOKEN}`, no embedded token) for Claude Code on the web (`install`) |
|
|
757
790
|
| `--force` | Overwrite existing skill files (`install`) |
|
|
758
791
|
| `--deep` | Write/read/delete round-trip (`doctor`) |
|
|
759
792
|
| `--json` | Machine-readable output (`list` / `search` / `show` / `stats` / `scopes` / `diff` / `tree` / `lint` / `dedupe` / `link`) |
|
|
@@ -882,3 +915,12 @@ at release time and is never committed to git. See
|
|
|
882
915
|
|
|
883
916
|
`install` writes your token into `.mcp.json`. Keep that file out of version
|
|
884
917
|
control (LoreKit's root `.gitignore` already ignores `.mcp.json`).
|
|
918
|
+
|
|
919
|
+
The **one exception is `install --mcp-json`**: that file authenticates via a
|
|
920
|
+
`${LOREKIT_TOKEN}` reference instead of an embedded token, so it holds no secret
|
|
921
|
+
and **is** meant to be committed (that is how Claude Code on the web reads it
|
|
922
|
+
after a fresh clone). You'll need to un-ignore `.mcp.json` first, since it is
|
|
923
|
+
normally git-ignored for the embedded-token reason above. The token itself comes
|
|
924
|
+
from the `LOREKIT_TOKEN` environment variable at runtime — set it as an
|
|
925
|
+
environment secret, never commit it — and once the file is tracked, keep using
|
|
926
|
+
`--mcp-json` so a later plain install never writes a token into it.
|
package/bin/lorekit.mjs
CHANGED
|
@@ -43,7 +43,8 @@ ${c.bold('Commands')}
|
|
|
43
43
|
(~/.claude); --project / --global choose non-interactively.
|
|
44
44
|
Also prompts whether to wire the hooks (all / read-only /
|
|
45
45
|
none); --hooks <mode> chooses non-interactively and --no-hooks
|
|
46
|
-
skips them (skills stay model-invoked only).
|
|
46
|
+
skips them (skills stay model-invoked only). --mcp-json also
|
|
47
|
+
writes a committable project .mcp.json for Claude Code on the web.
|
|
47
48
|
uninstall Reverse install: remove the lorekit-memory + lorekit-setup skills,
|
|
48
49
|
the MCP server entry, and the lifecycle hooks for the chosen scope. Surgical —
|
|
49
50
|
other servers, hooks, and settings are left untouched. Prompts
|
|
@@ -126,6 +127,8 @@ ${c.bold('Options')}
|
|
|
126
127
|
-y, --yes Non-interactive / apply; never prompt
|
|
127
128
|
--hooks <mode> Lifecycle hooks to wire: all | read-only | none (install)
|
|
128
129
|
--no-hooks Skip wiring the lifecycle hooks (install)
|
|
130
|
+
--mcp-json Also write a committable project .mcp.json for Claude Code on
|
|
131
|
+
the web — auth via \${LOREKIT_TOKEN}, no embedded secret (install)
|
|
129
132
|
--force Overwrite existing skill files (install)
|
|
130
133
|
--deep Do a write→read→delete round-trip (doctor)
|
|
131
134
|
--telemetry Verify the OTLP export credential works (doctor)
|
|
@@ -183,6 +186,14 @@ An interactive run preselects whatever is already wired, so re-running install
|
|
|
183
186
|
never resurrects hooks you declined. Answering "No hooks" (or --hooks none)
|
|
184
187
|
REMOVES hooks that are already there; --no-hooks only skips wiring new ones.
|
|
185
188
|
|
|
189
|
+
${c.bold('Claude Code on the web')}
|
|
190
|
+
Add --mcp-json to write a committable project .mcp.json (repo root) that Claude
|
|
191
|
+
Code on the web can see after a fresh clone. It authenticates via a
|
|
192
|
+
\${LOREKIT_TOKEN} reference in an mcp-remote --header — NOT an embedded token —
|
|
193
|
+
so the file is safe to commit; set LOREKIT_TOKEN as an environment secret in the
|
|
194
|
+
web UI. Pair it with --global to get the local CLI, skills, and hooks in
|
|
195
|
+
~/.claude AND the committable web config in one command.
|
|
196
|
+
|
|
186
197
|
${c.bold('Options')}
|
|
187
198
|
-d, --dir <path> Target project root (default: current directory)
|
|
188
199
|
--project Install into this project: .claude/skills + .mcp.json (default)
|
|
@@ -191,6 +202,8 @@ ${c.bold('Options')}
|
|
|
191
202
|
-t, --token <token> LoreKit token: lk_rw_* read+write, lk_ro_* read-only, lk_wo_* write-only
|
|
192
203
|
--hooks <mode> Wire the lifecycle hooks: all | read-only | none
|
|
193
204
|
--no-hooks Skip wiring the lifecycle hooks (leaves existing ones alone)
|
|
205
|
+
--mcp-json Also write a committable project .mcp.json (\${LOREKIT_TOKEN} auth)
|
|
206
|
+
for Claude Code on the web — always the repo-root file
|
|
194
207
|
--force Overwrite existing skill files
|
|
195
208
|
-y, --yes Non-interactive; never prompt (defaults to --project, and to the
|
|
196
209
|
already-wired hooks — all on a fresh install)
|
|
@@ -198,6 +211,8 @@ ${c.bold('Options')}
|
|
|
198
211
|
${c.bold('Examples')}
|
|
199
212
|
npx @lorekit/cli install --endpoint https://ref.supabase.co/functions/v1/mcp --token lk_rw_xxx
|
|
200
213
|
npx @lorekit/cli install --global
|
|
214
|
+
npx @lorekit/cli install --mcp-json --yes # web-ready project .mcp.json
|
|
215
|
+
npx @lorekit/cli install --global --mcp-json --yes # local CLI + committable web config
|
|
201
216
|
npx @lorekit/cli install --hooks read-only --yes
|
|
202
217
|
npx @lorekit/cli install --no-hooks --yes
|
|
203
218
|
`,
|
|
@@ -631,7 +646,7 @@ ${c.bold('Options')}
|
|
|
631
646
|
// typo like `--gloabl` should fail loudly, not quietly fall back to --project.
|
|
632
647
|
const KNOWN_FLAGS = [
|
|
633
648
|
'dir', 'project', 'global', 'endpoint', 'token', 'mode', 'store',
|
|
634
|
-
'from', 'to', 'apply', 'yes', 'hooks', 'no-hooks', 'force', 'deep', 'adapter',
|
|
649
|
+
'from', 'to', 'apply', 'yes', 'hooks', 'no-hooks', 'mcp-json', 'force', 'deep', 'adapter',
|
|
635
650
|
'event', 'json', 'scope', 'key', 'threshold', 'help', 'version', 'telemetry',
|
|
636
651
|
'value', 'tags', 'source-agent', 'trigger', 'kind', 'host', 'ttl-days', 'clear-ttl', 'org', 'remote', 'local',
|
|
637
652
|
'link', 'base', 'q', 'owner', 'range', 'view', 'archived',
|
|
@@ -662,7 +677,7 @@ async function main() {
|
|
|
662
677
|
const argv = process.argv.slice(2);
|
|
663
678
|
const args = parseArgs(argv, {
|
|
664
679
|
aliases: { d: 'dir', e: 'endpoint', t: 'token', y: 'yes', h: 'help', v: 'version' },
|
|
665
|
-
booleans: ['yes', 'force', 'deep', 'apply', 'help', 'version', 'global', 'project', 'no-hooks', 'no-origin', 'json', 'remote', 'local', 'link', 'archived', 'clear-ttl', 'telemetry', 'all'],
|
|
680
|
+
booleans: ['yes', 'force', 'deep', 'apply', 'help', 'version', 'global', 'project', 'no-hooks', 'mcp-json', 'no-origin', 'json', 'remote', 'local', 'link', 'archived', 'clear-ttl', 'telemetry', 'all'],
|
|
666
681
|
known: KNOWN_FLAGS,
|
|
667
682
|
});
|
|
668
683
|
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
|
|
7
8
|
// packages/cli/ — the installable package root (this file lives in src/).
|
|
@@ -366,6 +367,88 @@ export function upsertMcpServer(root, remoteUrl, scope = 'project') {
|
|
|
366
367
|
return { file, existed };
|
|
367
368
|
}
|
|
368
369
|
|
|
370
|
+
// The environment variable the committable (web) .mcp.json references for its
|
|
371
|
+
// token, instead of embedding a live secret. Kept as a constant so the writer,
|
|
372
|
+
// the install summary, and the docs can never name a different variable.
|
|
373
|
+
export const WEB_TOKEN_ENV_VAR = 'LOREKIT_TOKEN';
|
|
374
|
+
|
|
375
|
+
// Strip any credential from an endpoint URL before it goes into the committable
|
|
376
|
+
// web .mcp.json. The whole point of that file is to carry NO secret, so an
|
|
377
|
+
// `--endpoint` / LOREKIT_MCP_URL value that already embeds `?token=lk_…` (a
|
|
378
|
+
// perfectly ordinary thing to paste) must not be written verbatim — that would
|
|
379
|
+
// commit a live token in the file install tells you to commit. Mirrors
|
|
380
|
+
// `splitEndpoint`'s token removal (mcp.mjs) inline rather than importing it, so
|
|
381
|
+
// config.mjs keeps its no-mcp.mjs-import invariant (see resolveProjectConnection),
|
|
382
|
+
// and also clears userinfo for defense in depth. A non-URL string is left as-is;
|
|
383
|
+
// endpoint validity is the caller's concern, not this function's.
|
|
384
|
+
function stripEndpointCredentials(endpoint) {
|
|
385
|
+
try {
|
|
386
|
+
const u = new URL(endpoint);
|
|
387
|
+
u.searchParams.delete('token');
|
|
388
|
+
u.username = '';
|
|
389
|
+
u.password = '';
|
|
390
|
+
return u.toString();
|
|
391
|
+
} catch {
|
|
392
|
+
return endpoint;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Merge a lorekit server entry into the PROJECT-root .mcp.json in the
|
|
397
|
+
// committable form Claude Code on the web needs: instead of embedding the token
|
|
398
|
+
// in the URL (a live secret, which is why the embedded form is git-ignored), it
|
|
399
|
+
// authenticates via an mcp-remote `--header` that references `${LOREKIT_TOKEN}`.
|
|
400
|
+
//
|
|
401
|
+
// WHY this exists as a distinct writer. Claude Code on the web clones the repo
|
|
402
|
+
// fresh into an ephemeral container, so the only MCP config it can see is a
|
|
403
|
+
// COMMITTED, repo-root `.mcp.json` — a global `~/.claude.json` never travels to
|
|
404
|
+
// the clone. And it must be committable, which the embedded-token form is not.
|
|
405
|
+
// So the web path needs exactly this: the project file (never the global one)
|
|
406
|
+
// with the token supplied at runtime from an environment secret. Callers set
|
|
407
|
+
// `LOREKIT_TOKEN` as an environment secret in the web UI; the value is expanded
|
|
408
|
+
// by Claude Code before mcp-remote is spawned. Preserves any other servers.
|
|
409
|
+
//
|
|
410
|
+
// The endpoint is credential-stripped first: it is the one field that could
|
|
411
|
+
// carry a `?token=` in from `--endpoint` / LOREKIT_MCP_URL, and writing that
|
|
412
|
+
// into a file meant to be committed would leak a live token.
|
|
413
|
+
export function upsertWebMcpServer(root, endpoint, envVar = WEB_TOKEN_ENV_VAR) {
|
|
414
|
+
const file = mcpJsonPath(root); // always the project file — the web clone only sees this one
|
|
415
|
+
const config = readJsonIfExists(file) || {};
|
|
416
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
417
|
+
config.mcpServers = {};
|
|
418
|
+
}
|
|
419
|
+
const existed = Boolean(config.mcpServers.lorekit);
|
|
420
|
+
config.mcpServers.lorekit = {
|
|
421
|
+
command: 'npx',
|
|
422
|
+
args: ['-y', 'mcp-remote', stripEndpointCredentials(endpoint), '--header', `Authorization:Bearer \${${envVar}}`],
|
|
423
|
+
};
|
|
424
|
+
writeFileAtomic(file, JSON.stringify(config, null, 2) + '\n');
|
|
425
|
+
return { file, existed };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Is the project .mcp.json git-ignored in this repo? Tri-state so callers can
|
|
429
|
+
// distinguish "definitely ignored" from "can't tell" and stay quiet when unsure:
|
|
430
|
+
// true → ignored (git check-ignore matched) — the caller warns
|
|
431
|
+
// false → tracked / not ignored — nothing to say
|
|
432
|
+
// null → unknown (no git, not a repo, error) — say nothing
|
|
433
|
+
//
|
|
434
|
+
// This exists because the committable web .mcp.json (upsertWebMcpServer) only
|
|
435
|
+
// works if it is actually committed, and .mcp.json is COMMONLY git-ignored (the
|
|
436
|
+
// default embedded-token form is a secret, so LoreKit's own root .gitignore and
|
|
437
|
+
// many projects ignore it). Silently writing a file the repo will never commit
|
|
438
|
+
// is the trap this lets `install --mcp-json` warn about. `git check-ignore -q`
|
|
439
|
+
// exits 0 when ignored, 1 when not, 128 on error — anything but 0/1 is unknown.
|
|
440
|
+
export function isMcpJsonGitIgnored(root) {
|
|
441
|
+
try {
|
|
442
|
+
const r = spawnSync('git', ['-C', root, 'check-ignore', '-q', '.mcp.json'], { stdio: 'ignore' });
|
|
443
|
+
if (r.error) return null; // git not found / spawn failed
|
|
444
|
+
if (r.status === 0) return true;
|
|
445
|
+
if (r.status === 1) return false;
|
|
446
|
+
return null; // 128 (not a repo) or anything unexpected
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
369
452
|
// Pull the configured lorekit remote URL out of the project .mcp.json, if
|
|
370
453
|
// present. Non-throwing: returns null when the file is absent, invalid, or has
|
|
371
454
|
// no lorekit server. Callers that need to distinguish those use readMcpConfig.
|
|
@@ -406,6 +489,37 @@ export function removeMcpServer(root, scope = 'project') {
|
|
|
406
489
|
return { file, removed: true };
|
|
407
490
|
}
|
|
408
491
|
|
|
492
|
+
// Is a lorekit server entry the committable WEB form — auth via a `--header`
|
|
493
|
+
// that references an `${ENV_VAR}` token, as written by `upsertWebMcpServer` —
|
|
494
|
+
// rather than an embedded-token or plain URL entry? Used to gate the global
|
|
495
|
+
// uninstall's project-file cleanup so it only removes what `--mcp-json` wrote.
|
|
496
|
+
export function isWebMcpServerEntry(server) {
|
|
497
|
+
const args = server && Array.isArray(server.args) ? server.args : [];
|
|
498
|
+
const i = args.indexOf('--header');
|
|
499
|
+
if (i === -1 || typeof args[i + 1] !== 'string') return false;
|
|
500
|
+
return /^Authorization:Bearer \$\{[^}]+\}$/.test(args[i + 1].trim());
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Remove the lorekit entry from the PROJECT .mcp.json ONLY when it is the
|
|
504
|
+
// committable web form. `uninstall --global` calls this to clean up the file
|
|
505
|
+
// `install --global --mcp-json` wrote — WITHOUT touching an unrelated
|
|
506
|
+
// embedded-token `install --project` entry a user set up separately (which a
|
|
507
|
+
// blind removeMcpServer(root, 'project') would delete). No-op otherwise.
|
|
508
|
+
export function removeWebMcpServer(root) {
|
|
509
|
+
const file = mcpJsonPath(root);
|
|
510
|
+
const config = readJsonIfExists(file);
|
|
511
|
+
const server =
|
|
512
|
+
config && config.mcpServers && typeof config.mcpServers === 'object'
|
|
513
|
+
? config.mcpServers.lorekit
|
|
514
|
+
: null;
|
|
515
|
+
if (!server || !isWebMcpServerEntry(server)) return { file, removed: false };
|
|
516
|
+
|
|
517
|
+
delete config.mcpServers.lorekit;
|
|
518
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
519
|
+
writeFileAtomic(file, JSON.stringify(config, null, 2) + '\n');
|
|
520
|
+
return { file, removed: true };
|
|
521
|
+
}
|
|
522
|
+
|
|
409
523
|
// Strip lorekit hook entries from every event, preserving non-lorekit hooks in
|
|
410
524
|
// the same groups. Prunes groups left with no hooks and events left with no
|
|
411
525
|
// groups. Returns the count removed; only writes when something changed.
|
|
@@ -475,18 +589,29 @@ export function tokenKind(token) {
|
|
|
475
589
|
// install), then the `mcp.endpoint` field in .lorekit.json (committable URL
|
|
476
590
|
// without token — safe for VCS), then env. `splitEndpoint` is passed in to
|
|
477
591
|
// avoid a circular import with mcp.mjs.
|
|
592
|
+
//
|
|
593
|
+
// A source that STORES a token wins outright (project beats global — closest
|
|
594
|
+
// scope), but a TOKENLESS source must NOT shadow a later source that has one.
|
|
595
|
+
// That shadowing is exactly what `install --global --mcp-json` created: it
|
|
596
|
+
// writes a committable, token-free project .mcp.json (auth via ${LOREKIT_TOKEN})
|
|
597
|
+
// AND the real token into ~/.claude.json. Returning early on the project entry
|
|
598
|
+
// left the local CLI, doctor, and hooks resolving `token: null` unless
|
|
599
|
+
// LOREKIT_TOKEN was exported — so a tokenless source is only remembered as an
|
|
600
|
+
// endpoint fallback, and the loop keeps looking for a stored token.
|
|
478
601
|
export function resolveProjectConnection(root, splitEndpoint) {
|
|
479
602
|
const sources = [readLorekitServer(root), readServerFromFile(mcpConfigPath(root, 'global'))];
|
|
603
|
+
let endpointOnly = null; // closest usable endpoint that carried no token (e.g. the web .mcp.json)
|
|
480
604
|
for (const configured of sources) {
|
|
481
|
-
if (configured
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
605
|
+
if (!configured || !configured.url) continue;
|
|
606
|
+
const { endpoint, token } = splitEndpoint(configured.url);
|
|
607
|
+
if (!endpoint || endpoint.includes('<project-ref>')) continue;
|
|
608
|
+
if (token) return { endpoint, token }; // a stored token wins; project (closest) beats global
|
|
609
|
+
if (!endpointOnly) endpointOnly = endpoint; // remember the closest tokenless source, keep looking
|
|
610
|
+
}
|
|
611
|
+
if (endpointOnly) {
|
|
612
|
+
// No source stored a token — the committable web .mcp.json is exactly this
|
|
613
|
+
// case. Fall back to the env token so the local CLI still authenticates.
|
|
614
|
+
return { endpoint: endpointOnly, token: process.env.LOREKIT_TOKEN || null };
|
|
490
615
|
}
|
|
491
616
|
|
|
492
617
|
// Fallback: `mcp.endpoint` in .lorekit.json — a committable URL without token.
|
package/src/install.mjs
CHANGED
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
skillInstallDir,
|
|
10
10
|
copyDir,
|
|
11
11
|
upsertMcpServer,
|
|
12
|
+
upsertWebMcpServer,
|
|
13
|
+
isMcpJsonGitIgnored,
|
|
14
|
+
WEB_TOKEN_ENV_VAR,
|
|
12
15
|
upsertClaudeHooks,
|
|
13
16
|
resolveHookRunner,
|
|
14
17
|
HOOK_MODES,
|
|
@@ -194,6 +197,12 @@ export async function install(args) {
|
|
|
194
197
|
// circuits). `--no-hooks` is skip-only and never justifies that bypass.
|
|
195
198
|
const hooksFlagExplicit = typeof args.hooks === 'string' && args.hooks.trim() !== '';
|
|
196
199
|
|
|
200
|
+
// `--mcp-json` writes a committable, env-var-referencing project .mcp.json for
|
|
201
|
+
// Claude Code on the web (see upsertWebMcpServer). It is an explicit intent to
|
|
202
|
+
// (re)write that file, so — like an explicit `--hooks` — it must reach the
|
|
203
|
+
// write step even when the scope is already fully installed.
|
|
204
|
+
const writeWebMcpJson = Boolean(args['mcp-json']);
|
|
205
|
+
|
|
197
206
|
heading('LoreKit install');
|
|
198
207
|
log(` project: ${c.dim(root)}`);
|
|
199
208
|
|
|
@@ -219,7 +228,7 @@ export async function install(args) {
|
|
|
219
228
|
|
|
220
229
|
const wiredEvents = installedHookEvents(root, scope);
|
|
221
230
|
|
|
222
|
-
if (currentState.isFullyInstalled && !force && !hooksFlagExplicit) {
|
|
231
|
+
if (currentState.isFullyInstalled && !force && !hooksFlagExplicit && !writeWebMcpJson) {
|
|
223
232
|
// Surface a clear, useful already-installed summary.
|
|
224
233
|
log('');
|
|
225
234
|
log(
|
|
@@ -345,8 +354,28 @@ export async function install(args) {
|
|
|
345
354
|
});
|
|
346
355
|
|
|
347
356
|
// 5. Wire the MCP config for the chosen scope.
|
|
357
|
+
// When `--mcp-json` is going to (re)write the PROJECT .mcp.json in its
|
|
358
|
+
// committable form and the scope target IS that same file (project scope),
|
|
359
|
+
// skip this write — otherwise we would write an embedded-token file only to
|
|
360
|
+
// overwrite it with the committable one a moment later. A global scope
|
|
361
|
+
// targets ~/.claude.json, a different file, so it always writes.
|
|
348
362
|
const remoteUrl = buildRemoteUrl(endpoint, token);
|
|
349
|
-
const
|
|
363
|
+
const scopeWriteOwnedByWeb = writeWebMcpJson && scope === 'project';
|
|
364
|
+
let file = null;
|
|
365
|
+
let existed = false;
|
|
366
|
+
if (!scopeWriteOwnedByWeb) {
|
|
367
|
+
({ file, existed } = upsertMcpServer(root, remoteUrl, scope));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 5a. `--mcp-json`: write the committable, env-var-referencing project
|
|
371
|
+
// .mcp.json for Claude Code on the web (see upsertWebMcpServer). Always
|
|
372
|
+
// the repo-root file, regardless of scope — that is the only MCP config a
|
|
373
|
+
// fresh web clone can see. The token is NOT embedded here: the file
|
|
374
|
+
// references `${LOREKIT_TOKEN}`, set as an environment secret in the web UI.
|
|
375
|
+
let webMcp = null;
|
|
376
|
+
if (writeWebMcpJson) {
|
|
377
|
+
webMcp = upsertWebMcpServer(root, endpoint, WEB_TOKEN_ENV_VAR);
|
|
378
|
+
}
|
|
350
379
|
|
|
351
380
|
// 5b. Hooks — the deterministic layer the Claude plugin adds on top of the
|
|
352
381
|
// skill, firing the shared `lorekit hook` engine (which reads the same
|
|
@@ -427,7 +456,30 @@ export async function install(args) {
|
|
|
427
456
|
: 'already up to date';
|
|
428
457
|
status(s.existed && s.written === 0 ? 'info' : 'pass', `skill ${s.name}`, `${skillState} → ${display(s.dest)}`);
|
|
429
458
|
}
|
|
430
|
-
|
|
459
|
+
if (file) {
|
|
460
|
+
status('pass', mcpLabel, `${existed ? 'updated' : 'created'} lorekit server → ${display(file)}`);
|
|
461
|
+
}
|
|
462
|
+
if (webMcp) {
|
|
463
|
+
// The committable web form: reported separately so it is clear this file is
|
|
464
|
+
// meant to be committed (unlike the embedded-token form) and that the token
|
|
465
|
+
// comes from an environment secret rather than the file.
|
|
466
|
+
const webPath = path.relative(root, webMcp.file) || webMcp.file;
|
|
467
|
+
status(
|
|
468
|
+
'pass',
|
|
469
|
+
'.mcp.json (web)',
|
|
470
|
+
`${webMcp.existed ? 'updated' : 'created'} committable lorekit server (auth via \${${WEB_TOKEN_ENV_VAR}}) → ${webPath}`,
|
|
471
|
+
);
|
|
472
|
+
// The web file only works if it is actually committed, and .mcp.json is
|
|
473
|
+
// commonly git-ignored (the embedded-token form is a secret). Warn when it
|
|
474
|
+
// is, so a fresh web clone silently missing the config is not a mystery.
|
|
475
|
+
if (isMcpJsonGitIgnored(root) === true) {
|
|
476
|
+
status(
|
|
477
|
+
'warn',
|
|
478
|
+
'.mcp.json (git)',
|
|
479
|
+
'git-ignored — un-ignore it (add `!.mcp.json` to .gitignore, or `git add -f .mcp.json`) or a fresh web clone will not see it',
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
431
483
|
|
|
432
484
|
if (!touchHooks) {
|
|
433
485
|
status(
|
|
@@ -465,8 +517,29 @@ export async function install(args) {
|
|
|
465
517
|
}
|
|
466
518
|
|
|
467
519
|
const kind = tokenKind(token);
|
|
468
|
-
if (
|
|
469
|
-
|
|
520
|
+
if (scopeWriteOwnedByWeb) {
|
|
521
|
+
// `--project --mcp-json`: the committable web .mcp.json is the ONLY config
|
|
522
|
+
// written, and it references ${LOREKIT_TOKEN} rather than storing a token —
|
|
523
|
+
// so NOTHING persists a credential here. Reporting the token's tier would
|
|
524
|
+
// imply it was configured; instead say it is not stored (and that a passed
|
|
525
|
+
// --token was therefore not persisted), pointing at the environment secret.
|
|
526
|
+
status(
|
|
527
|
+
'warn',
|
|
528
|
+
'token',
|
|
529
|
+
`not stored — the committable .mcp.json resolves \${${WEB_TOKEN_ENV_VAR}} at runtime; set it as an environment secret${
|
|
530
|
+
token ? ' (any --token you passed is not persisted)' : ''
|
|
531
|
+
}`,
|
|
532
|
+
);
|
|
533
|
+
} else if (kind === 'none') {
|
|
534
|
+
// A global --mcp-json with no token still writes the web file; explain the
|
|
535
|
+
// runtime resolution rather than implying reads/writes will fail.
|
|
536
|
+
status(
|
|
537
|
+
'warn',
|
|
538
|
+
'token',
|
|
539
|
+
webMcp
|
|
540
|
+
? `none stored — the committable .mcp.json resolves \${${WEB_TOKEN_ENV_VAR}} at runtime; set it as an environment secret`
|
|
541
|
+
: 'none configured — reads/writes will fail until a token is set',
|
|
542
|
+
);
|
|
470
543
|
} else if (kind === 'read-only') {
|
|
471
544
|
status('warn', 'token', 'read-only (lk_ro_*) — the skill can read memories but not write them');
|
|
472
545
|
} else if (kind === 'write-only') {
|
|
@@ -485,15 +558,26 @@ export async function install(args) {
|
|
|
485
558
|
}
|
|
486
559
|
|
|
487
560
|
log(`\n Next: ${c.cyan('npx @lorekit/cli doctor')} to verify the connection.`);
|
|
488
|
-
|
|
561
|
+
// Where the token lives, and how to treat that file, depends on which MCP
|
|
562
|
+
// configs were written. The web `.mcp.json` never holds the token, so a
|
|
563
|
+
// project scope that was owned by `--mcp-json` gets the web guidance, not the
|
|
564
|
+
// "keep it out of version control" note that applies to the embedded form.
|
|
565
|
+
if (webMcp) {
|
|
489
566
|
log(
|
|
490
567
|
` ${c.dim(
|
|
491
|
-
|
|
492
|
-
? 'Note: your token is stored in ~/.claude.json (used by every project) — keep that file private.'
|
|
493
|
-
: 'Note: your token is stored in .mcp.json — keep it out of version control.',
|
|
568
|
+
`For Claude Code on the web: commit .mcp.json (it is often git-ignored — un-ignore it first), then set ${WEB_TOKEN_ENV_VAR} as an environment secret. The file references the token, so it never holds the secret itself.`,
|
|
494
569
|
)}`,
|
|
495
570
|
);
|
|
496
571
|
}
|
|
572
|
+
if (token && scope === 'global') {
|
|
573
|
+
log(
|
|
574
|
+
` ${c.dim('Note: your token is stored in ~/.claude.json (used by every project) — keep that file private.')}`,
|
|
575
|
+
);
|
|
576
|
+
} else if (token && scope === 'project' && !scopeWriteOwnedByWeb) {
|
|
577
|
+
log(
|
|
578
|
+
` ${c.dim('Note: your token is stored in .mcp.json — keep it out of version control.')}`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
497
581
|
// Bounded, non-PII: which of the three presets this run landed on. Counting
|
|
498
582
|
// the `--no-hooks` FLAG (as telemetry already did) says nothing about what a
|
|
499
583
|
// user picks when actually asked, which is the whole point of the prompt.
|
package/src/mcp-server.mjs
CHANGED
|
@@ -146,6 +146,17 @@ export const MEMORY_TOOL_DEFS = [
|
|
|
146
146
|
description: 'Soft-archive a memory. Hidden from reads but restorable.',
|
|
147
147
|
inputSchema: { type: 'object', required: ['scope', 'key'] },
|
|
148
148
|
},
|
|
149
|
+
{
|
|
150
|
+
name: 'memory.scopes',
|
|
151
|
+
// The description tells the model WHEN to reach for this, not just what it
|
|
152
|
+
// returns: every other read tool needs a scope named up front, so this is
|
|
153
|
+
// the one that answers "what is there?" before you can ask "what is in it?".
|
|
154
|
+
description:
|
|
155
|
+
'List every scope in the store with how many active memories it holds — '
|
|
156
|
+
+ 'the inventory to consult when you do not already know which scope to read. '
|
|
157
|
+
+ 'Takes no arguments and is store-wide, NOT limited to the current directory.',
|
|
158
|
+
inputSchema: { type: 'object', properties: {} },
|
|
159
|
+
},
|
|
149
160
|
];
|
|
150
161
|
|
|
151
162
|
// Org tools — always advertised regardless of memory mode. They always route
|
|
@@ -215,8 +226,117 @@ const MEMORY_DISPATCH = {
|
|
|
215
226
|
'memory.search': (store, a) => store.search(a),
|
|
216
227
|
'memory.delete': (store, a) => store.delete(a),
|
|
217
228
|
'memory.archive': (store, a) => store.archive(a),
|
|
229
|
+
'memory.scopes': (store) => listScopes(store),
|
|
218
230
|
};
|
|
219
231
|
|
|
232
|
+
// `memory.scopes` — the store-wide inventory, normalised.
|
|
233
|
+
//
|
|
234
|
+
// This exists because an agent that cannot enumerate scopes cannot know what it
|
|
235
|
+
// does not know. `memory.list` and `memory.search` both need a scope (or a
|
|
236
|
+
// scope list) up front, so without this the only reachable lore is the lore
|
|
237
|
+
// whose scope the agent could already name — and the SessionStart injection is
|
|
238
|
+
// deliberately a bounded slice, not an index of the whole store. `GET
|
|
239
|
+
// /memories/scopes` and the `lorekit scopes` command have answered this since
|
|
240
|
+
// migration 00039; the MCP surface was the one caller that could not ask.
|
|
241
|
+
//
|
|
242
|
+
// THE TWO STORES ANSWER IN DIFFERENT SHAPES, and normalising here is the whole
|
|
243
|
+
// job of this function. `LocalStore`/`TwoTierStore.listScopes()` return a BARE
|
|
244
|
+
// ARRAY (`[{ scope, count }]`), while `RemoteStore.listScopes()` returns the
|
|
245
|
+
// standard `{ ok, scopes }` envelope — or `{ ok: false, error, networkError,
|
|
246
|
+
// unusable }`. A tool that passed either through verbatim would hand the model
|
|
247
|
+
// two different contracts for one tool name depending on a config value it
|
|
248
|
+
// cannot see.
|
|
249
|
+
//
|
|
250
|
+
// DEGRADATION IS EXIT-CLEAN, mirroring the `scopes` command, which reports an
|
|
251
|
+
// unreachable remote as a short note at exit 0 rather than failing the run. An
|
|
252
|
+
// inventory that cannot be built is `{ scopes: [], note }` with `ok: true`, so
|
|
253
|
+
// `toolResult` does NOT mark it `isError`: "I could not enumerate" is a fact
|
|
254
|
+
// about the store, not a failed tool call, and a model that receives a
|
|
255
|
+
// tool-level error is liable to retry it rather than carry on with the lore it
|
|
256
|
+
// can already reach. The note says which, in bounded, non-PII terms.
|
|
257
|
+
export async function listScopes(store) {
|
|
258
|
+
let res;
|
|
259
|
+
try {
|
|
260
|
+
res = await store.listScopes();
|
|
261
|
+
} catch (e) {
|
|
262
|
+
// A store that cannot enumerate must not take the session down with it.
|
|
263
|
+
return { ok: true, scopes: [], note: `scope enumeration failed: ${errText(e)}` };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Local/two-tier: the bare array form.
|
|
267
|
+
if (Array.isArray(res)) return { ok: true, scopes: sortScopes(res.map(shapeScope)) };
|
|
268
|
+
|
|
269
|
+
// Remote: the envelope form.
|
|
270
|
+
if (res && res.ok) {
|
|
271
|
+
return { ok: true, scopes: sortScopes((Array.isArray(res.scopes) ? res.scopes : []).map(shapeScope)) };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { ok: true, scopes: [], note: scopeFailureNote(res) };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Sorted by scope ascending, which is the contract `docs/mcp-tools.md`, the
|
|
278
|
+
// tool catalog and `llms.txt` all state for `memory.scopes`. The HOSTED surface
|
|
279
|
+
// gets that ordering from `lorekit_memory_scopes` (`order by m.scope asc`,
|
|
280
|
+
// migration 00039/00049), but `LocalStore`/`TwoTierStore.listScopes()` both
|
|
281
|
+
// return their `Map` insertion order — a walk order, not an ordering — so the
|
|
282
|
+
// stdio server owns it here rather than the two surfaces answering differently.
|
|
283
|
+
// Sorting BOTH shapes (not just the local one) makes the guarantee a property
|
|
284
|
+
// of this function instead of an assumption about the store it was handed.
|
|
285
|
+
// Codepoint comparison, deliberately not `localeCompare`: the ordering must not
|
|
286
|
+
// depend on the HOST's locale.
|
|
287
|
+
//
|
|
288
|
+
// That is ascending-by-scope, not byte-identical parity with the hosted path,
|
|
289
|
+
// and the difference is worth being precise about. `order by m.scope asc` sorts
|
|
290
|
+
// under the DATABASE's collation (`en_US.UTF-8` on a default Supabase project),
|
|
291
|
+
// which does not order like codepoint around punctuation — and a scope string
|
|
292
|
+
// is mostly punctuation (`::`, `/`, `-`), so `repo::a-b` and `repo::ab` can come
|
|
293
|
+
// out in the opposite relative order on the two surfaces. Case cannot differ
|
|
294
|
+
// (every scope segment is lowercased, see docs/scope-format.md). Closing the
|
|
295
|
+
// remaining gap means `collate "C"` on the RPC's `order by`, which changes the
|
|
296
|
+
// order `GET /memories/scopes` has always returned — a public contract change
|
|
297
|
+
// that belongs in its own migration, not here. Until then: both surfaces are
|
|
298
|
+
// sorted ascending, neither is unordered, and nothing should depend on the two
|
|
299
|
+
// agreeing on the exact position of a punctuated neighbour.
|
|
300
|
+
function sortScopes(rows) {
|
|
301
|
+
return rows.sort((a, b) => (a.scope < b.scope ? -1 : a.scope > b.scope ? 1 : 0));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// One inventory row. `last_activity` is passed through when the store supplied
|
|
305
|
+
// it (the hosted `GET /memories/scopes` has returned it since migration 00049)
|
|
306
|
+
// and OMITTED — never null — when it did not, so a client can tell "this store
|
|
307
|
+
// does not report freshness" from "this scope has no activity".
|
|
308
|
+
function shapeScope(s) {
|
|
309
|
+
const scope = String(s?.scope ?? '');
|
|
310
|
+
const count = Number(s?.count);
|
|
311
|
+
const row = { scope, count: Number.isFinite(count) ? count : 0 };
|
|
312
|
+
const last = s?.last_activity ?? s?.lastActivity;
|
|
313
|
+
return last ? { ...row, last_activity: last } : row;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// A short, bounded reason an enumeration produced nothing. Deliberately built
|
|
317
|
+
// here rather than reusing `lessons-view.mjs`'s `describeError`: that module
|
|
318
|
+
// carries the whole render/`util` stack, and this server has kept clear of it.
|
|
319
|
+
// The vocabulary matches what that helper reports, so the two read alike.
|
|
320
|
+
function scopeFailureNote(res) {
|
|
321
|
+
if (!res) return 'the store returned no result';
|
|
322
|
+
if (res.unusable) return 'no usable store is configured';
|
|
323
|
+
if (res.networkError) return `network error: ${String(res.networkError).slice(0, 200)}`;
|
|
324
|
+
// `httpStatus` is the ONLY field that carries a real status: `restFetch`'s
|
|
325
|
+
// error object is `{ message, code }`, and `code` is the response body's own
|
|
326
|
+
// application code on a JSON error, so rendering it as "HTTP <code>" would
|
|
327
|
+
// print a non-status. Read the top-level field first — that is the one
|
|
328
|
+
// `RemoteStore.listScopes()` passes through — and keep the nested read as a
|
|
329
|
+
// tolerance for any store that nests it instead.
|
|
330
|
+
const status = res.httpStatus ?? res.error?.httpStatus;
|
|
331
|
+
if (status) return `request failed with HTTP ${status}`;
|
|
332
|
+
if (res.error?.message) return String(res.error.message).slice(0, 200);
|
|
333
|
+
return 'the store could not enumerate its scopes';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function errText(e) {
|
|
337
|
+
return String(e?.message ?? e).slice(0, 200);
|
|
338
|
+
}
|
|
339
|
+
|
|
220
340
|
// Provenance for a tool call: the caller's explicit values win, the working
|
|
221
341
|
// directory and CI environment fill the rest. Best-effort — a failure to shell
|
|
222
342
|
// out to git must never fail the write, so it degrades to no origin at all.
|
package/src/store/remote.mjs
CHANGED
|
@@ -217,11 +217,43 @@ class RemoteStore {
|
|
|
217
217
|
// is not relied upon (the server sorts by scope asc; the view re-sorts by
|
|
218
218
|
// scope type). Failures use this store's standard `{ ok:false, error,
|
|
219
219
|
// networkError }` envelope so the caller can degrade gracefully.
|
|
220
|
+
//
|
|
221
|
+
// `httpStatus` is carried through VERBATIM from `restFetch`, which is the ONLY
|
|
222
|
+
// place the real status lives: its error object holds `{ message, code }`,
|
|
223
|
+
// where `code` is the response body's own application code on a JSON error
|
|
224
|
+
// (a string like `permission_denied`) and only incidentally the status on a
|
|
225
|
+
// non-JSON one. A consumer that wants to say "HTTP 403" must therefore read
|
|
226
|
+
// `httpStatus`, never `error.code` — `mcp-server.mjs`'s `scopeFailureNote`
|
|
227
|
+
// does exactly that, and it had nothing to read until this field was passed
|
|
228
|
+
// through. Additive: `scopes.mjs`, `stats.mjs` and `lessons-view.mjs` all
|
|
229
|
+
// branch on `ok` / `unusable` / `networkError` and ignore the extra key.
|
|
220
230
|
async listScopes() {
|
|
221
231
|
const res = await this._rest('/memories/scopes');
|
|
222
|
-
if (!res.ok)
|
|
232
|
+
if (!res.ok) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
error: res.error,
|
|
236
|
+
httpStatus: res.httpStatus,
|
|
237
|
+
networkError: res.networkError,
|
|
238
|
+
unusable: res.unusable,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
223
241
|
const scopes = Array.isArray(res.data?.scopes) ? res.data.scopes : [];
|
|
224
|
-
|
|
242
|
+
// `last_activity` (migration 00049) is `max(created_at)` over exactly the
|
|
243
|
+
// counted rows — per-scope freshness without listing rows to reduce them,
|
|
244
|
+
// which is the row-cap trap this endpoint exists to avoid. It is passed
|
|
245
|
+
// through when present and OMITTED when absent (an older backend, or the
|
|
246
|
+
// offline store, which has no equivalent), so a consumer can tell "this
|
|
247
|
+
// store does not report freshness" from "this scope has none". Callers that
|
|
248
|
+
// read only `{ scope, count }` — `scopes.mjs`, `stats.mjs` — are unaffected.
|
|
249
|
+
return {
|
|
250
|
+
ok: true,
|
|
251
|
+
scopes: scopes.map((s) => ({
|
|
252
|
+
scope: s.scope,
|
|
253
|
+
count: Number(s.count) || 0,
|
|
254
|
+
...(s.last_activity ? { last_activity: s.last_activity } : {}),
|
|
255
|
+
})),
|
|
256
|
+
};
|
|
225
257
|
}
|
|
226
258
|
|
|
227
259
|
// Authentication probe for doctor — does the configured token STILL work?
|
package/src/uninstall.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveProjectRoot,
|
|
10
10
|
removeSkill,
|
|
11
11
|
removeMcpServer,
|
|
12
|
+
removeWebMcpServer,
|
|
12
13
|
removeClaudeHooks,
|
|
13
14
|
homeDir,
|
|
14
15
|
} from './config.mjs';
|
|
@@ -56,6 +57,15 @@ export async function uninstall(args) {
|
|
|
56
57
|
step: attempt(() => removeSkill(root, scope, skill.name)),
|
|
57
58
|
}));
|
|
58
59
|
const mcp = attempt(() => removeMcpServer(root, scope));
|
|
60
|
+
// `install --global --mcp-json` writes the committable project .mcp.json in
|
|
61
|
+
// ADDITION to ~/.claude.json, so a GLOBAL uninstall must also clear that
|
|
62
|
+
// entry or it orphans a lorekit server pointing at a now-removed setup. A
|
|
63
|
+
// project uninstall already targets .mcp.json via `mcp` above, so this is
|
|
64
|
+
// global-only. `removeWebMcpServer` removes ONLY the committable web form, so
|
|
65
|
+
// it never deletes an unrelated embedded-token `install --project` entry a
|
|
66
|
+
// user set up separately. Surgical and idempotent: no web file ⇒ a quiet
|
|
67
|
+
// "nothing to remove", other servers in the file are preserved.
|
|
68
|
+
const webMcp = scope === 'global' ? attempt(() => removeWebMcpServer(root)) : null;
|
|
59
69
|
const hooks = attempt(() => removeClaudeHooks(root, scope));
|
|
60
70
|
|
|
61
71
|
// Global paths shown relative to ~; project paths repo-relative.
|
|
@@ -74,15 +84,26 @@ export async function uninstall(args) {
|
|
|
74
84
|
done: (r) => `lorekit server removed → ${display(r.file)}`,
|
|
75
85
|
noop: 'no lorekit server entry — nothing to remove',
|
|
76
86
|
});
|
|
87
|
+
if (webMcp) {
|
|
88
|
+
// Always the repo-relative project path, even under a global uninstall.
|
|
89
|
+
report(webMcp, '.mcp.json (web)', {
|
|
90
|
+
done: (r) => `lorekit server removed → ${path.relative(root, r.file) || r.file}`,
|
|
91
|
+
noop: 'no committable project .mcp.json — nothing to remove',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
77
94
|
report(hooks, 'hooks', {
|
|
78
95
|
done: (r) => `${r.removed} removed → ${display(r.file)}`,
|
|
79
96
|
noop: 'no lorekit hooks — nothing to remove',
|
|
80
97
|
});
|
|
81
98
|
|
|
82
99
|
const skillStepList = skillSteps.map((s) => s.step);
|
|
83
|
-
const
|
|
100
|
+
const webSteps = webMcp ? [webMcp] : [];
|
|
101
|
+
const failed = [...skillStepList, mcp, ...webSteps, hooks].some((s) => !s.ok);
|
|
84
102
|
const any =
|
|
85
|
-
(skillStepList.some((s) => s.result?.removed) ||
|
|
103
|
+
(skillStepList.some((s) => s.result?.removed) ||
|
|
104
|
+
mcp.result?.removed ||
|
|
105
|
+
webMcp?.result?.removed ||
|
|
106
|
+
hooks.result?.removed) && true;
|
|
86
107
|
|
|
87
108
|
if (failed) {
|
|
88
109
|
log(`\n ${c.dim('Some items could not be removed and were left untouched — see above.')}`);
|