@lorekit/cli 1.35.0 → 1.36.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/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/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.')}`);
|