@lorekit/cli 1.3.0 → 1.4.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/bin/lorekit.mjs CHANGED
@@ -83,25 +83,164 @@ ${c.bold('Examples')}
83
83
  npx @lorekit/cli doctor --deep
84
84
  npx @lorekit/cli migrate --from .lore # preview a rename
85
85
  npx @lorekit/cli migrate --from .lore --to project --yes
86
+
87
+ Run ${c.cyan('lorekit <command> --help')} for command-specific options.
86
88
  `;
87
89
 
90
+ // Per-command help. Keyed by command; `lorekit <command> --help` prints the
91
+ // focused entry instead of the full top-level HELP, so a user only sees the
92
+ // flags that actually apply to what they're running.
93
+ const COMMAND_HELP = {
94
+ install: `${c.bold('lorekit install')} — scaffold the skill, wire the MCP server, install the hooks
95
+
96
+ ${c.bold('Usage')}
97
+ npx @lorekit/cli install [options]
98
+
99
+ Scaffolds the lorekit-memory skill, adds the LoreKit MCP server, and wires the
100
+ deterministic hooks (lessons on SessionStart, a nudge on tool failure + at Stop).
101
+
102
+ ${c.bold('Options')}
103
+ -d, --dir <path> Target project root (default: current directory)
104
+ --project Install into this project: .claude/skills + .mcp.json (default)
105
+ --global Install for every project: ~/.claude/skills + ~/.claude.json
106
+ -e, --endpoint <url> LoreKit MCP endpoint (else LOREKIT_MCP_URL)
107
+ -t, --token <token> LoreKit token: lk_rw_* read+write, lk_ro_* read-only, lk_wo_* write-only
108
+ --no-hooks Skip wiring the lifecycle hooks (skill stays model-invoked only)
109
+ --force Overwrite existing skill files
110
+ -y, --yes Non-interactive; never prompt (defaults to --project)
111
+
112
+ ${c.bold('Examples')}
113
+ npx @lorekit/cli install --endpoint https://ref.supabase.co/functions/v1/mcp --token lk_rw_xxx
114
+ npx @lorekit/cli install --global
115
+ npx @lorekit/cli install --no-hooks --yes
116
+ `,
117
+ uninstall: `${c.bold('lorekit uninstall')} — reverse install for the chosen scope
118
+
119
+ ${c.bold('Usage')}
120
+ npx @lorekit/cli uninstall [options]
121
+
122
+ Removes the lorekit-memory skill, the MCP server entry, and the lifecycle hooks.
123
+ Surgical — other servers, hooks, and settings are left untouched.
124
+
125
+ ${c.bold('Options')}
126
+ -d, --dir <path> Target project root (default: current directory)
127
+ --project Uninstall from this project (default)
128
+ --global Uninstall the global (~/.claude) setup
129
+ -y, --yes Non-interactive; never prompt
130
+
131
+ ${c.bold('Examples')}
132
+ npx @lorekit/cli uninstall --global
133
+ npx @lorekit/cli uninstall --project --yes
134
+ `,
135
+ doctor: `${c.bold('lorekit doctor')} — verify the skill install and the resolved memory backend
136
+
137
+ ${c.bold('Usage')}
138
+ npx @lorekit/cli doctor [options]
139
+
140
+ Checks the node runtime, skill install, resolved memory mode, MCP connectivity,
141
+ token, and scope.
142
+
143
+ ${c.bold('Options')}
144
+ -d, --dir <path> Target project root (default: current directory)
145
+ --mode <mode> Override the resolved mode: off | local | remote
146
+ -e, --endpoint <url> Endpoint override (else .mcp.json / LOREKIT_MCP_URL)
147
+ -t, --token <token> Token override (else .mcp.json / LOREKIT_TOKEN)
148
+ --store <path> Local project-tier store directory (default: .lorekit)
149
+ --deep Do a write→read→delete round-trip
150
+
151
+ ${c.bold('Examples')}
152
+ npx @lorekit/cli doctor
153
+ npx @lorekit/cli doctor --deep
154
+ npx @lorekit/cli doctor --mode local
155
+ `,
156
+ migrate: `${c.bold('lorekit migrate')} — relocate a LoreKit-format local store into the current layout
157
+
158
+ ${c.bold('Usage')}
159
+ npx @lorekit/cli migrate --from <path> [options]
160
+
161
+ Dry-run by default; pass --yes (or --apply) to write. Idempotent.
162
+
163
+ ${c.bold('Options')}
164
+ -d, --dir <path> Target project root (default: current directory)
165
+ --from <path> Source store to migrate from (required)
166
+ --to <tier> Destination tier: home | project (default routes by scope)
167
+ --apply Apply the migration (alias of --yes)
168
+ -y, --yes Apply the migration; never prompt
169
+
170
+ ${c.bold('Examples')}
171
+ npx @lorekit/cli migrate --from .lore # preview a rename
172
+ npx @lorekit/cli migrate --from .lore --to project --yes
173
+ `,
174
+ hook: `${c.bold('lorekit hook')} — hook engine for Claude Code / Cursor / Codex
175
+
176
+ ${c.bold('Usage')}
177
+ lorekit hook --adapter <claude|cursor|codex> --event <name> [--dir <path>]
178
+
179
+ Machine-facing: reads the host's JSON on stdin and injects lessons or a
180
+ retrospective nudge on stdout, always exiting 0. Wired into a plugin's hook
181
+ config by \`lorekit install\` — not run by hand.
182
+
183
+ ${c.bold('Options')}
184
+ -d, --dir <path> Target project root
185
+ --adapter <name> Host framework: claude | cursor | codex
186
+ --event <name> Host hook event (else read from the stdin payload)
187
+ `,
188
+ mcp: `${c.bold('lorekit mcp')} — local stdio MCP server
189
+
190
+ ${c.bold('Usage')}
191
+ lorekit mcp [--dir <path>]
192
+
193
+ Machine-facing: exposes the memory.* tools backed by the resolved store (local
194
+ .lorekit/ offline, or remote passthrough) over JSON-RPC on stdin/stdout, so
195
+ .mcp.json can point at the CLI instead of mcp-remote. Not run by hand.
196
+
197
+ ${c.bold('Options')}
198
+ -d, --dir <path> Target project root (default: current directory)
199
+ `,
200
+ };
201
+
202
+ // Every long flag the CLI understands (after alias resolution). Passed to the
203
+ // parser so an unrecognized flag is captured rather than silently ignored — a
204
+ // typo like `--gloabl` should fail loudly, not quietly fall back to --project.
205
+ const KNOWN_FLAGS = [
206
+ 'dir', 'project', 'global', 'endpoint', 'token', 'mode', 'store',
207
+ 'from', 'to', 'apply', 'yes', 'no-hooks', 'force', 'deep', 'adapter',
208
+ 'event', 'help', 'version',
209
+ ];
210
+
211
+ // Commands that write to disk / talk to the network on a human's behalf. These
212
+ // reject unknown flags; the machine-facing `hook` / `mcp` do not (they must
213
+ // never fail on a stray flag, and only ever receive flags we control).
214
+ const HUMAN_COMMANDS = new Set(['install', 'uninstall', 'doctor', 'migrate']);
215
+
88
216
  async function main() {
89
217
  const argv = process.argv.slice(2);
90
218
  const args = parseArgs(argv, {
91
219
  aliases: { d: 'dir', e: 'endpoint', t: 'token', y: 'yes', h: 'help', v: 'version' },
92
220
  booleans: ['yes', 'force', 'deep', 'apply', 'help', 'version', 'global', 'project', 'no-hooks'],
221
+ known: KNOWN_FLAGS,
93
222
  });
94
223
 
224
+ const command = args._[0];
225
+
226
+ // Help is intercepted first — before the machine-facing hook/mcp dispatch — so
227
+ // `lorekit <command> --help` always documents the command (even hook/mcp)
228
+ // instead of blocking on stdin. Real hook/mcp invocations never pass --help.
229
+ if (args.help) {
230
+ log(command && COMMAND_HELP[command] ? COMMAND_HELP[command] : HELP);
231
+ return 0;
232
+ }
233
+
95
234
  // `hook` is machine-facing: it must never print help/errors to stdout
96
235
  // (that would corrupt the JSON the host parses). Handle it before the
97
- // help/usage branch and always resolve to exit 0.
98
- if (args._[0] === 'hook') {
236
+ // usage branch and always resolve to exit 0.
237
+ if (command === 'hook') {
99
238
  return hook(args);
100
239
  }
101
240
 
102
241
  // `mcp` is machine-facing too: only JSON-RPC frames may reach stdout, so it
103
- // must bypass the help/usage branch. It serves stdio until the client closes.
104
- if (args._[0] === 'mcp') {
242
+ // must bypass the usage branch. It serves stdio until the client closes.
243
+ if (command === 'mcp') {
105
244
  return mcpServer(args);
106
245
  }
107
246
 
@@ -110,11 +249,18 @@ async function main() {
110
249
  return 0;
111
250
  }
112
251
 
113
- const command = args._[0];
114
-
115
- if (args.help || !command) {
252
+ if (!command) {
116
253
  log(HELP);
117
- return command ? 0 : args.help ? 0 : 1;
254
+ return 1;
255
+ }
256
+
257
+ // Reject unrecognized flags on human-facing commands with an actionable
258
+ // pointer, rather than silently ignoring a typo that would change behavior.
259
+ if (HUMAN_COMMANDS.has(command) && args._unknown.length > 0) {
260
+ const plural = args._unknown.length > 1 ? 's' : '';
261
+ err(`${c.red(`Unknown option${plural}:`)} ${args._unknown.join(', ')}`);
262
+ err(`Run ${c.cyan(`lorekit ${command} --help`)} to see valid options.`);
263
+ return 1;
118
264
  }
119
265
 
120
266
  // Human-facing commands are wrapped so we can see which commands people run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.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/util.mjs CHANGED
@@ -119,8 +119,13 @@ export function select(question, options, { defaultIndex = 0 } = {}) {
119
119
 
120
120
  // Minimal flag parser: --key value, --key=value, -k value, and bare --flags.
121
121
  // `aliases` maps short → long; `booleans` lists flags that take no value.
122
- export function parseArgs(argv, { aliases = {}, booleans = [] } = {}) {
122
+ // When `known` is a non-null list of long flag names, any flag whose resolved
123
+ // name isn't in it is collected (as its original token) into `out._unknown` —
124
+ // letting the caller reject typos like `--gloabl` instead of silently ignoring
125
+ // them. `out._unknown` is only present when `known` was supplied.
126
+ export function parseArgs(argv, { aliases = {}, booleans = [], known = null } = {}) {
123
127
  const out = { _: [] };
128
+ const unknown = [];
124
129
  for (let i = 0; i < argv.length; i++) {
125
130
  let token = argv[i];
126
131
  if (!token.startsWith('-')) {
@@ -135,6 +140,7 @@ export function parseArgs(argv, { aliases = {}, booleans = [] } = {}) {
135
140
  }
136
141
  let key = token.replace(/^-+/, '');
137
142
  if (aliases[key]) key = aliases[key];
143
+ if (known && !known.includes(key)) unknown.push(token);
138
144
  if (booleans.includes(key)) {
139
145
  out[key] = true;
140
146
  continue;
@@ -150,5 +156,6 @@ export function parseArgs(argv, { aliases = {}, booleans = [] } = {}) {
150
156
  }
151
157
  out[key] = value;
152
158
  }
159
+ if (known) out._unknown = unknown;
153
160
  return out;
154
161
  }