@lorekit/cli 1.7.1 → 1.8.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 +32 -2
- package/bin/lorekit.mjs +41 -4
- package/package.json +1 -1
- package/src/lessons-view.mjs +109 -0
- package/src/list.mjs +123 -0
- package/src/stores.mjs +49 -0
- package/src/telemetry.mjs +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,34 @@ lorekit doctor --deep # also does a write → read → delete round-trip (ne
|
|
|
106
106
|
|
|
107
107
|
Exit code is non-zero if any check fails, so it fits CI gates.
|
|
108
108
|
|
|
109
|
+
### `lorekit list` (alias `ls`)
|
|
110
|
+
|
|
111
|
+
Shows the lessons that apply to **where you are** — the scopes `deriveScope`
|
|
112
|
+
resolves for the current directory (`project::{name}`, `branch::…`, `repo::…`,
|
|
113
|
+
and `global`) — split into two clearly-labelled sections so you can see where
|
|
114
|
+
each lesson lives:
|
|
115
|
+
|
|
116
|
+
- **Offline** — the local two-tier store (`.lorekit/` in the repo + `~/.lorekit/`).
|
|
117
|
+
- **Remote** — the hosted MCP store. When no token/endpoint is configured this
|
|
118
|
+
section is a short note on how to set it up; it is **never an error** (the
|
|
119
|
+
command still exits 0 and shows your offline lessons). A network/server error
|
|
120
|
+
is likewise a per-scope warning, not a crash.
|
|
121
|
+
|
|
122
|
+
It is read-only — it never writes, deletes, or reveals archived lessons — and it
|
|
123
|
+
independently queries both stores regardless of the resolved memory mode. A
|
|
124
|
+
`LOREKIT_DENY=remote` (or `local`) ceiling suppresses that section, honoring the
|
|
125
|
+
same deny-wins privacy invariant the control model enforces for agents.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
lorekit list # both sections, grouped by scope
|
|
129
|
+
lorekit ls # same, via the alias
|
|
130
|
+
lorekit list --scope global # narrow to a single scope
|
|
131
|
+
lorekit list --json # structured { offline, remote } payload for scripts
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`--endpoint` / `--token` override the remote connection; `--store` overrides the
|
|
135
|
+
local project-tier directory.
|
|
136
|
+
|
|
109
137
|
### `lorekit hook`
|
|
110
138
|
|
|
111
139
|
The **shared hook engine** behind the Claude Code / Cursor / Codex plugins.
|
|
@@ -288,6 +316,8 @@ active deny constraints.
|
|
|
288
316
|
| `--no-hooks` | Skip wiring the lifecycle hooks; skill + MCP only (`install`) |
|
|
289
317
|
| `--force` | Overwrite existing skill files (`install`) |
|
|
290
318
|
| `--deep` | Write/read/delete round-trip (`doctor`) |
|
|
319
|
+
| `--json` | Machine-readable output (`list`) |
|
|
320
|
+
| `--scope <scope>` | Restrict to a single scope (`list`; default: all applicable) |
|
|
291
321
|
| `--adapter <name>` | Host framework for `hook`: `claude` / `cursor` / `codex` |
|
|
292
322
|
| `--event <name>` | Host hook event for `hook` (else read from the stdin payload) |
|
|
293
323
|
| `-h, --help` | Help |
|
|
@@ -372,8 +402,8 @@ forever. This reduces manual validation to a single capture pass per tool.
|
|
|
372
402
|
|
|
373
403
|
## Usage telemetry
|
|
374
404
|
|
|
375
|
-
The human-facing commands (`install`, `uninstall`, `doctor`, `migrate`)
|
|
376
|
-
OpenTelemetry span + one counter point per run so the maintainers can see which
|
|
405
|
+
The human-facing commands (`install`, `uninstall`, `doctor`, `list`, `migrate`)
|
|
406
|
+
emit one OpenTelemetry span + one counter point per run so the maintainers can see which
|
|
377
407
|
commands people use. It is zero-dependency (OTLP/JSON over `fetch`, no SDK) and
|
|
378
408
|
deliberately narrow — it carries only the command name, a bounded set of boolean
|
|
379
409
|
flags (`--global`, `--deep`, …), the CLI/runtime/OS identity, and the outcome.
|
package/bin/lorekit.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { parseArgs, log, err, c } from '../src/util.mjs';
|
|
|
6
6
|
import { install } from '../src/install.mjs';
|
|
7
7
|
import { uninstall } from '../src/uninstall.mjs';
|
|
8
8
|
import { doctor } from '../src/doctor.mjs';
|
|
9
|
+
import { list } from '../src/list.mjs';
|
|
9
10
|
import { hook } from '../src/hook.mjs';
|
|
10
11
|
import { migrate } from '../src/migrate.mjs';
|
|
11
12
|
import { mcpServer } from '../src/mcp-server.mjs';
|
|
@@ -35,6 +36,10 @@ ${c.bold('Commands')}
|
|
|
35
36
|
other servers, hooks, and settings are left untouched. Prompts
|
|
36
37
|
project vs global; --project / --global choose non-interactively.
|
|
37
38
|
doctor Verify the skill install, MCP connectivity, token, and scope.
|
|
39
|
+
list (ls) List the lessons that apply to the current directory, split into
|
|
40
|
+
an Offline section (local .lorekit/ + ~/.lorekit/) and a Remote
|
|
41
|
+
section (hosted MCP). Groups by scope (project/branch/repo/global).
|
|
42
|
+
--json for scripting, --scope <s> to narrow.
|
|
38
43
|
migrate Relocate a LoreKit-format local store into the current layout.
|
|
39
44
|
Dry-run by default; pass --yes to apply. Idempotent.
|
|
40
45
|
hook Hook engine for Claude Code / Cursor / Codex. Reads the host's
|
|
@@ -53,6 +58,8 @@ ${c.bold('Options')}
|
|
|
53
58
|
-t, --token <token> LoreKit token (lk_rw_* to allow writes, lk_ro_* read-only)
|
|
54
59
|
--mode <mode> Memory mode: off | local | remote (doctor override)
|
|
55
60
|
--store <path> Local project-tier store directory (default: .lorekit)
|
|
61
|
+
--json Machine-readable output (list)
|
|
62
|
+
--scope <scope> Restrict to a single scope (list)
|
|
56
63
|
--from <path> Source store to migrate from (migrate)
|
|
57
64
|
--to <tier> Migration destination tier: home | project (migrate;
|
|
58
65
|
default routes each entry by scope)
|
|
@@ -156,6 +163,30 @@ ${c.bold('Examples')}
|
|
|
156
163
|
npx @lorekit/cli doctor
|
|
157
164
|
npx @lorekit/cli doctor --deep
|
|
158
165
|
npx @lorekit/cli doctor --mode local
|
|
166
|
+
`,
|
|
167
|
+
list: `${c.bold('lorekit list')} — list the lessons that apply to the current directory ${c.dim('(alias: ls)')}
|
|
168
|
+
|
|
169
|
+
${c.bold('Usage')}
|
|
170
|
+
npx @lorekit/cli list [options]
|
|
171
|
+
|
|
172
|
+
Shows the lessons for the scopes that resolve for the current directory
|
|
173
|
+
(project/branch/repo/global), split into an Offline section (the local
|
|
174
|
+
.lorekit/ + ~/.lorekit/ two-tier store) and a Remote section (the hosted MCP
|
|
175
|
+
server). When no remote token/endpoint is configured the Remote section is a
|
|
176
|
+
short note on how to set it up — never an error.
|
|
177
|
+
|
|
178
|
+
${c.bold('Options')}
|
|
179
|
+
-d, --dir <path> Target project root (default: current directory)
|
|
180
|
+
--scope <scope> Restrict to a single scope (default: all applicable)
|
|
181
|
+
--json Machine-readable output
|
|
182
|
+
-e, --endpoint <url> Remote endpoint override (else .mcp.json / LOREKIT_MCP_URL)
|
|
183
|
+
-t, --token <token> Remote token override (else .mcp.json / LOREKIT_TOKEN)
|
|
184
|
+
--store <path> Local project-tier store directory (default: .lorekit)
|
|
185
|
+
|
|
186
|
+
${c.bold('Examples')}
|
|
187
|
+
npx @lorekit/cli list
|
|
188
|
+
npx @lorekit/cli list --json
|
|
189
|
+
npx @lorekit/cli list --scope global
|
|
159
190
|
`,
|
|
160
191
|
migrate: `${c.bold('lorekit migrate')} — relocate a LoreKit-format local store into the current layout
|
|
161
192
|
|
|
@@ -209,13 +240,17 @@ ${c.bold('Options')}
|
|
|
209
240
|
const KNOWN_FLAGS = [
|
|
210
241
|
'dir', 'project', 'global', 'endpoint', 'token', 'mode', 'store',
|
|
211
242
|
'from', 'to', 'apply', 'yes', 'no-hooks', 'force', 'deep', 'adapter',
|
|
212
|
-
'event', 'help', 'version',
|
|
243
|
+
'event', 'json', 'scope', 'help', 'version',
|
|
213
244
|
];
|
|
214
245
|
|
|
215
246
|
// Commands that write to disk / talk to the network on a human's behalf. These
|
|
216
247
|
// reject unknown flags; the machine-facing `hook` / `mcp` do not (they must
|
|
217
248
|
// never fail on a stray flag, and only ever receive flags we control).
|
|
218
|
-
const HUMAN_COMMANDS = new Set(['install', 'uninstall', 'doctor', 'migrate']);
|
|
249
|
+
const HUMAN_COMMANDS = new Set(['install', 'uninstall', 'doctor', 'list', 'migrate']);
|
|
250
|
+
|
|
251
|
+
// Command aliases — canonicalized before help / dispatch so `lorekit ls --help`
|
|
252
|
+
// and telemetry both resolve to the real command name.
|
|
253
|
+
const COMMAND_ALIASES = { ls: 'list' };
|
|
219
254
|
|
|
220
255
|
async function main() {
|
|
221
256
|
// Load a `.env` from the current directory (if any) before anything reads the
|
|
@@ -227,11 +262,11 @@ async function main() {
|
|
|
227
262
|
const argv = process.argv.slice(2);
|
|
228
263
|
const args = parseArgs(argv, {
|
|
229
264
|
aliases: { d: 'dir', e: 'endpoint', t: 'token', y: 'yes', h: 'help', v: 'version' },
|
|
230
|
-
booleans: ['yes', 'force', 'deep', 'apply', 'help', 'version', 'global', 'project', 'no-hooks'],
|
|
265
|
+
booleans: ['yes', 'force', 'deep', 'apply', 'help', 'version', 'global', 'project', 'no-hooks', 'json'],
|
|
231
266
|
known: KNOWN_FLAGS,
|
|
232
267
|
});
|
|
233
268
|
|
|
234
|
-
const command = args._[0];
|
|
269
|
+
const command = COMMAND_ALIASES[args._[0]] || args._[0];
|
|
235
270
|
|
|
236
271
|
// Help is intercepted first — before the machine-facing hook/mcp dispatch — so
|
|
237
272
|
// `lorekit <command> --help` always documents the command (even hook/mcp)
|
|
@@ -284,6 +319,8 @@ async function main() {
|
|
|
284
319
|
return traceCommand('uninstall', args, VERSION, () => uninstall(args));
|
|
285
320
|
case 'doctor':
|
|
286
321
|
return traceCommand('doctor', args, VERSION, () => doctor(args));
|
|
322
|
+
case 'list':
|
|
323
|
+
return traceCommand('list', args, VERSION, () => list(args));
|
|
287
324
|
case 'migrate':
|
|
288
325
|
return traceCommand('migrate', args, VERSION, () => migrate(args));
|
|
289
326
|
default:
|
package/package.json
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Shared, store-agnostic view layer for the read commands (`list`, and the
|
|
2
|
+
// stacked `search` / `show` / `stats` / `diff` to come). Two responsibilities:
|
|
3
|
+
// 1. pure helpers — the applicable-scope set, entry normalization, previews;
|
|
4
|
+
// 2. `gather()` — collect a store's entries per scope behind the common
|
|
5
|
+
// store contract; and `renderSection()` — print one offline/remote section.
|
|
6
|
+
//
|
|
7
|
+
// Kept separate from `list.mjs` on purpose: the offline/remote sectioned
|
|
8
|
+
// rendering is a reusable seam, not a `list`-only detail. Zero-dependency.
|
|
9
|
+
import { log, heading, status, c } from './util.mjs';
|
|
10
|
+
|
|
11
|
+
// The scopes that apply to the current directory, most-specific → broadest:
|
|
12
|
+
// project, branch, repo, global. De-duplicated (a repo with no branch scope,
|
|
13
|
+
// or a project whose name collides, never lists a scope twice). Pure — takes an
|
|
14
|
+
// already-derived `deriveScope()` result so it is trivially unit-testable.
|
|
15
|
+
export function scopeList({ projectScope, branchScope, repoScope } = {}) {
|
|
16
|
+
return [...new Set([projectScope, branchScope, repoScope, 'global'].filter(Boolean))];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Normalize an entry from either store (local markdown row or hosted DB row)
|
|
20
|
+
// into one stable shape the view + `--json` output can rely on. Remote rows may
|
|
21
|
+
// spell the timestamp `updated_at`; local rows use `updated`.
|
|
22
|
+
export function normalizeEntry(e = {}) {
|
|
23
|
+
return {
|
|
24
|
+
scope: e.scope ?? null,
|
|
25
|
+
key: e.key ?? null,
|
|
26
|
+
value: e.value == null ? '' : String(e.value),
|
|
27
|
+
updated: e.updated ?? e.updated_at ?? null,
|
|
28
|
+
tags: Array.isArray(e.tags) ? e.tags : [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// A single-line, whitespace-collapsed, length-bounded preview of a lesson body.
|
|
33
|
+
export function preview(value, max = 72) {
|
|
34
|
+
const s = String(value == null ? '' : value)
|
|
35
|
+
.replace(/\s+/g, ' ')
|
|
36
|
+
.trim();
|
|
37
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Just the calendar date from an ISO timestamp, for compact `(updated …)` tags.
|
|
41
|
+
export function shortDate(iso) {
|
|
42
|
+
const s = String(iso || '');
|
|
43
|
+
return /^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A bounded, non-PII description of a per-scope read failure (network / server).
|
|
47
|
+
function describeError(res) {
|
|
48
|
+
if (!res) return 'no response';
|
|
49
|
+
if (res.networkError) return String(res.networkError);
|
|
50
|
+
if (res.error) return res.error.message || `error ${res.error.code}`;
|
|
51
|
+
return 'unreadable';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Collect a store's non-archived entries for each scope, via the common
|
|
55
|
+
// `store.list({scope})` contract. Returns ordered per-scope groups plus a total
|
|
56
|
+
// — a per-scope read failure is captured on the group, never thrown, so one bad
|
|
57
|
+
// scope can't abort the listing. `store` may be a local or remote store.
|
|
58
|
+
export async function gather(store, scopes) {
|
|
59
|
+
const groups = [];
|
|
60
|
+
let total = 0;
|
|
61
|
+
for (const scope of scopes) {
|
|
62
|
+
let res;
|
|
63
|
+
try {
|
|
64
|
+
res = await store.list({ scope });
|
|
65
|
+
} catch (e) {
|
|
66
|
+
res = { ok: false, networkError: (e && e.message) || 'error' };
|
|
67
|
+
}
|
|
68
|
+
if (!res || res.ok === false) {
|
|
69
|
+
groups.push({ scope, entries: [], error: describeError(res) });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const entries = (res.entries || []).map(normalizeEntry);
|
|
73
|
+
total += entries.length;
|
|
74
|
+
groups.push({ scope, entries, error: null });
|
|
75
|
+
}
|
|
76
|
+
return { groups, total };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Render one section (Offline or Remote). `section` is either
|
|
80
|
+
// { available:false, reason } → a graceful note, or
|
|
81
|
+
// { available:true, groups, total } → grouped lessons.
|
|
82
|
+
export function renderSection(header, section) {
|
|
83
|
+
heading(header.title);
|
|
84
|
+
if (header.subtitle) log(` ${c.dim(header.subtitle)}`);
|
|
85
|
+
|
|
86
|
+
if (!section.available) {
|
|
87
|
+
status('warn', 'unavailable', section.reason);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const printable = (section.groups || []).filter((g) => g.entries.length || g.error);
|
|
92
|
+
if (!printable.length) {
|
|
93
|
+
log(` ${c.dim('no lessons found in the applicable scopes')}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const g of printable) {
|
|
98
|
+
log(` ${c.bold(g.scope)}`);
|
|
99
|
+
if (g.error) {
|
|
100
|
+
log(` ${c.yellow('!')} ${c.dim(g.error)}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const e of g.entries) {
|
|
104
|
+
const when = e.updated ? ` ${c.dim(`(updated ${shortDate(e.updated)})`)}` : '';
|
|
105
|
+
log(` ${c.cyan('•')} ${e.key}${when}`);
|
|
106
|
+
if (e.value) log(` ${c.dim(preview(e.value))}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/list.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// `lorekit list` — show the lessons that apply to the current directory, split
|
|
2
|
+
// into an Offline section (the local two-tier store) and a Remote section (the
|
|
3
|
+
// hosted MCP store). Scopes searched are the ones `deriveScope` resolves for the
|
|
4
|
+
// cwd: project, branch, repo, and global.
|
|
5
|
+
//
|
|
6
|
+
// Graceful by design: when no remote token/endpoint is configured the Remote
|
|
7
|
+
// section is a short note on how to set it up — never an error (mirrors
|
|
8
|
+
// `doctor`'s tone). Human-facing, so it is wrapped in `traceCommand` by the bin.
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
import { resolveProjectRoot } from './config.mjs';
|
|
13
|
+
import { deriveScope } from './scope.mjs';
|
|
14
|
+
import { loadControl } from './control.mjs';
|
|
15
|
+
import { resolveStores, remoteUnavailableReason } from './stores.mjs';
|
|
16
|
+
import { scopeList, gather, renderSection } from './lessons-view.mjs';
|
|
17
|
+
import { log, heading, c } from './util.mjs';
|
|
18
|
+
|
|
19
|
+
// Abbreviate the user's home directory to `~` for readable paths.
|
|
20
|
+
function prettyPath(p) {
|
|
21
|
+
const home = os.homedir();
|
|
22
|
+
return p && home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A human label for where the offline store reads from.
|
|
26
|
+
function offlineSubtitle(local, root) {
|
|
27
|
+
const home = prettyPath(local.homeDir);
|
|
28
|
+
if (!local.projectActive()) return `${home} (home tier only — no project .lorekit/ yet)`;
|
|
29
|
+
const projRel = path.relative(root, local.projectDir) || local.projectDir;
|
|
30
|
+
return `${projRel} + ${home}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function list(args) {
|
|
34
|
+
const root = resolveProjectRoot(args.dir);
|
|
35
|
+
const env = { ...process.env };
|
|
36
|
+
if (args.store) env.LOREKIT_STORE = args.store;
|
|
37
|
+
|
|
38
|
+
const scopeInfo = deriveScope(root);
|
|
39
|
+
// Default to every applicable scope; `--scope <s>` narrows to one (an explicit
|
|
40
|
+
// scope outside the applicable set is honoured — the user asked for it).
|
|
41
|
+
const scopes = args.scope && typeof args.scope === 'string' ? [args.scope] : scopeList(scopeInfo);
|
|
42
|
+
|
|
43
|
+
const { local, remote, connection } = resolveStores(root, {
|
|
44
|
+
env,
|
|
45
|
+
endpoint: args.endpoint,
|
|
46
|
+
token: args.token,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// A `LOREKIT_DENY=remote|local` privacy/compliance ceiling (deny-wins, never
|
|
50
|
+
// overridable) suppresses that section entirely — the same invariant the
|
|
51
|
+
// agent-facing control model enforces, honored here in the human read view.
|
|
52
|
+
const control = loadControl(root, { env });
|
|
53
|
+
const deny = (mode) => control.denies.find((d) => d.mode === mode) || null;
|
|
54
|
+
const localDenied = deny('local');
|
|
55
|
+
const remoteDenied = deny('remote');
|
|
56
|
+
|
|
57
|
+
const offline = localDenied ? { groups: [], total: 0 } : await gather(local, scopes);
|
|
58
|
+
const remoteAvailable = !remoteDenied && remote.usable();
|
|
59
|
+
const remoteResult = remoteAvailable ? await gather(remote, scopes) : { groups: [], total: 0 };
|
|
60
|
+
|
|
61
|
+
const offlineSection = localDenied
|
|
62
|
+
? { available: false, reason: `disabled by deny constraint (${localDenied.source})` }
|
|
63
|
+
: { available: true, ...offline };
|
|
64
|
+
const remoteSection = remoteAvailable
|
|
65
|
+
? { available: true, ...remoteResult }
|
|
66
|
+
: {
|
|
67
|
+
available: false,
|
|
68
|
+
reason: remoteDenied
|
|
69
|
+
? `disabled by deny constraint (${remoteDenied.source})`
|
|
70
|
+
: remoteUnavailableReason(connection),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (args.json) {
|
|
74
|
+
log(JSON.stringify(buildJson({ root, scopes, offlineSection, remoteSection }), null, 2));
|
|
75
|
+
} else {
|
|
76
|
+
heading('LoreKit lessons');
|
|
77
|
+
log(` project: ${c.dim(root)}`);
|
|
78
|
+
log(` scopes: ${scopes.join(' → ')}`);
|
|
79
|
+
|
|
80
|
+
renderSection(
|
|
81
|
+
{ title: 'Offline', subtitle: localDenied ? undefined : offlineSubtitle(local, root) },
|
|
82
|
+
offlineSection,
|
|
83
|
+
);
|
|
84
|
+
renderSection(
|
|
85
|
+
{ title: 'Remote', subtitle: remoteAvailable ? connection.endpoint : undefined },
|
|
86
|
+
remoteSection,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
log('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Bounded, non-PII telemetry extras (counts + a boolean) — never a scope
|
|
93
|
+
// string, path, key, or token.
|
|
94
|
+
return {
|
|
95
|
+
exitCode: 0,
|
|
96
|
+
'lorekit.cli.list.scope_count': scopes.length,
|
|
97
|
+
'lorekit.cli.list.offline_count': offline.total,
|
|
98
|
+
'lorekit.cli.list.remote_count': remoteResult.total,
|
|
99
|
+
'lorekit.cli.list.remote_available': remoteAvailable,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The `--json` payload: normalized groups per section so a script gets the same
|
|
104
|
+
// shape regardless of which store an entry came from.
|
|
105
|
+
function buildJson({ root, scopes, offlineSection, remoteSection }) {
|
|
106
|
+
return { root, scopes, offline: sectionJson(offlineSection), remote: sectionJson(remoteSection) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sectionJson(section) {
|
|
110
|
+
if (!section.available) {
|
|
111
|
+
return { available: false, reason: section.reason, total: 0, groups: [] };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
available: true,
|
|
115
|
+
total: section.total,
|
|
116
|
+
groups: (section.groups || []).map((g) => ({
|
|
117
|
+
scope: g.scope,
|
|
118
|
+
error: g.error || null,
|
|
119
|
+
// `gather()` already normalized these entries; no second pass needed.
|
|
120
|
+
entries: g.entries,
|
|
121
|
+
})),
|
|
122
|
+
};
|
|
123
|
+
}
|
package/src/stores.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Resolve BOTH memory stores at once — the offline (local two-tier) store and
|
|
2
|
+
// the remote (hosted MCP) store — independent of the active control mode. The
|
|
3
|
+
// read commands (`list`, and the stacked `search` / `show` / `stats` / `diff`)
|
|
4
|
+
// want to show whatever lives in each place side by side, so unlike
|
|
5
|
+
// `createStore(control)` (which picks exactly one by mode) this builds both.
|
|
6
|
+
//
|
|
7
|
+
// Nothing here invents scope or connection logic: it composes the existing
|
|
8
|
+
// `localStoreDirs` (two-tier dirs), `resolveProjectConnection` + `splitEndpoint`
|
|
9
|
+
// (endpoint/token from .mcp.json → global → env), and the store builders.
|
|
10
|
+
// Zero-dependency.
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
import { localStoreDirs } from './control.mjs';
|
|
13
|
+
import { resolveProjectConnection } from './config.mjs';
|
|
14
|
+
import { splitEndpoint } from './mcp.mjs';
|
|
15
|
+
import { createTwoTierStore, createRemoteStore } from './store/index.mjs';
|
|
16
|
+
|
|
17
|
+
// Build both stores for `root`. Explicit `endpoint` / `token` (e.g. from
|
|
18
|
+
// `--endpoint` / `--token` flags) take precedence over the resolved connection;
|
|
19
|
+
// `env` feeds the local-tier directory resolution ($LOREKIT_HOME / $LOREKIT_STORE).
|
|
20
|
+
// Returns { local, remote, connection } — `connection` is the resolved
|
|
21
|
+
// endpoint/token so callers can explain why remote is unusable.
|
|
22
|
+
export function resolveStores(root, { env = process.env, endpoint = null, token = null } = {}) {
|
|
23
|
+
const dirs = localStoreDirs(root, env);
|
|
24
|
+
const local = createTwoTierStore(dirs);
|
|
25
|
+
|
|
26
|
+
const conn = resolveProjectConnection(root, splitEndpoint);
|
|
27
|
+
const resolvedEndpoint = endpoint || conn.endpoint || null;
|
|
28
|
+
const resolvedToken = token || conn.token || null;
|
|
29
|
+
const remote = createRemoteStore({ endpoint: resolvedEndpoint, token: resolvedToken });
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
local,
|
|
33
|
+
remote,
|
|
34
|
+
connection: { endpoint: resolvedEndpoint, token: resolvedToken },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A bounded, actionable reason a remote listing is unavailable — mirrors the
|
|
39
|
+
// checks in `RemoteStore.usable()` so the note tells the user what to fix.
|
|
40
|
+
export function remoteUnavailableReason({ endpoint, token } = {}) {
|
|
41
|
+
if (!endpoint) return 'no endpoint configured — run `lorekit install` or set LOREKIT_MCP_URL';
|
|
42
|
+
if (String(endpoint).includes('<project-ref>')) return `endpoint is still a placeholder (${endpoint})`;
|
|
43
|
+
if (!token) return 'no token configured — set LOREKIT_TOKEN or run `lorekit install`';
|
|
44
|
+
// Defensive catch-all: unreachable while this mirrors `RemoteStore.usable()` —
|
|
45
|
+
// callers only reach here when the remote is unusable, so one check above has
|
|
46
|
+
// already fired. Kept as a total return (never a throw): this helper is on the
|
|
47
|
+
// graceful read path and must never break the listing.
|
|
48
|
+
return 'not configured';
|
|
49
|
+
}
|
package/src/telemetry.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const DEFAULT_DATASET = 'lorekit-cli';
|
|
|
34
34
|
|
|
35
35
|
// Flags worth counting (e.g. how many installs are --global). Bounded on
|
|
36
36
|
// purpose: only these booleans are ever attached, never free-form values.
|
|
37
|
-
const FLAG_ATTRS = ['global', 'project', 'deep', 'yes', 'force', 'no-hooks'];
|
|
37
|
+
const FLAG_ATTRS = ['global', 'project', 'deep', 'yes', 'force', 'no-hooks', 'json'];
|
|
38
38
|
|
|
39
39
|
const OFF_VALUES = new Set(['0', 'off', 'false', 'no', 'disable', 'disabled']);
|
|
40
40
|
|