@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.35.0",
3
+ "version": "1.36.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": {
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 && configured.url) {
482
- const { endpoint, token } = splitEndpoint(configured.url);
483
- if (endpoint && !endpoint.includes('<project-ref>')) {
484
- return {
485
- endpoint,
486
- token: token || process.env.LOREKIT_TOKEN || null,
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 { file, existed } = upsertMcpServer(root, remoteUrl, scope);
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
- status('pass', mcpLabel, `${existed ? 'updated' : 'created'} lorekit server → ${display(file)}`);
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 (kind === 'none') {
469
- status('warn', 'token', 'none configured reads/writes will fail until a token is set');
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
- if (token) {
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
- scope === 'global'
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 failed = [...skillStepList, mcp, hooks].some((s) => !s.ok);
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) || mcp.result?.removed || hooks.result?.removed) && true;
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.')}`);