@lorekit/cli 1.14.0 → 1.15.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 +48 -8
- package/bin/lorekit.mjs +31 -31
- package/package.json +1 -1
- package/src/config.mjs +31 -2
- package/src/control.mjs +45 -2
- package/src/core/lessons.mjs +34 -7
- package/src/dedupe.mjs +17 -3
- package/src/doctor.mjs +23 -5
- package/src/hook.mjs +5 -2
- package/src/install.mjs +4 -4
- package/src/lessons-view.mjs +3 -3
- package/src/list.mjs +1 -1
- package/src/mcp-server.mjs +7 -7
- package/src/scopes.mjs +1 -1
- package/src/search.mjs +1 -1
- package/src/show.mjs +2 -2
- package/src/telemetry.mjs +11 -2
- package/src/tree.mjs +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ markdown files if you never want to sign up for anything.
|
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
npm install -g @lorekit/cli
|
|
20
|
-
lorekit install # wires up the
|
|
20
|
+
lorekit install # wires up the skills, MCP server, and hooks
|
|
21
21
|
lorekit doctor # verify it's all green
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -47,7 +47,7 @@ Linux, and Windows (npm creates the `lorekit` shim on every platform).
|
|
|
47
47
|
Sets up the full memory loop — the same three parts as the Claude plugin,
|
|
48
48
|
without needing a marketplace:
|
|
49
49
|
|
|
50
|
-
1. **
|
|
50
|
+
1. **Skills** (`lorekit-memory` + `lorekit-setup`) — the model-invoked authoring judgment and the self-improvement loop authoring counterpart.
|
|
51
51
|
2. **MCP server** (`lorekit`) — the connection to your lessons, merged into the
|
|
52
52
|
MCP config (preserving any other servers).
|
|
53
53
|
3. **Hooks** — the *deterministic* layer: lessons injected on every
|
|
@@ -75,7 +75,7 @@ lorekit install --global # set it up for every project
|
|
|
75
75
|
|
|
76
76
|
In a TTY it prompts for the scope (and for `--endpoint` / `--token` if missing).
|
|
77
77
|
Flags: `--project` / `--global` pick the scope non-interactively; `--no-hooks`
|
|
78
|
-
installs the
|
|
78
|
+
installs the skills + MCP only (memory stays model-invoked); `--yes` runs
|
|
79
79
|
non-interactively (endpoint required via flag/env; scope defaults to project);
|
|
80
80
|
`--force` overwrites an existing skill copy. Re-running is idempotent — the hook
|
|
81
81
|
entries are updated in place, never duplicated.
|
|
@@ -444,13 +444,53 @@ Two config layers decide the mode:
|
|
|
444
444
|
- **Repo / team** — a `.lorekit.json` at the repo root (and/or the existing
|
|
445
445
|
`lorekit` block in `.mcp.json` for the connection).
|
|
446
446
|
|
|
447
|
-
Both files share
|
|
447
|
+
Both files share this schema — all fields optional:
|
|
448
448
|
|
|
449
449
|
```jsonc
|
|
450
|
+
// .lorekit.json (repo root — safe to commit, no secrets)
|
|
451
|
+
// ~/.lorekit/config.json (user/machine — personal overrides, not committed)
|
|
450
452
|
{
|
|
451
|
-
|
|
452
|
-
"
|
|
453
|
-
"
|
|
453
|
+
// ── Mode & store ───────────────────────────────────────────────────────────
|
|
454
|
+
"mode": "local", // off | local | remote
|
|
455
|
+
"store": ".lorekit", // project-tier store path (relative to root, or absolute)
|
|
456
|
+
"deny": ["remote"], // forbid modes outright — deny always wins, union across layers
|
|
457
|
+
|
|
458
|
+
// ── Connection (repo config only — no token, safe to commit) ───────────────
|
|
459
|
+
"mcp.endpoint": "https://<ref>.supabase.co/functions/v1/mcp",
|
|
460
|
+
// committable MCP URL without token; token still comes
|
|
461
|
+
// from .mcp.json or LOREKIT_TOKEN env var
|
|
462
|
+
|
|
463
|
+
// ── Write behaviour ────────────────────────────────────────────────────────
|
|
464
|
+
"tags.default": ["team", "project::my-project"],
|
|
465
|
+
// tags appended to every memory.write from this repo/user
|
|
466
|
+
// both layers merged: repo tags first, then user tags
|
|
467
|
+
|
|
468
|
+
"scope.defaults": {
|
|
469
|
+
"repo::owner/name": { "tags": ["team"] },
|
|
470
|
+
"branch::owner/name::": { "tags": ["ephemeral"] }
|
|
471
|
+
},
|
|
472
|
+
// per-scope tag defaults applied to writes whose scope
|
|
473
|
+
// starts with the key; matched by prefix (no wildcards needed)
|
|
474
|
+
// repo config only — this is a team-level write policy
|
|
475
|
+
|
|
476
|
+
// ── Hook behaviour ─────────────────────────────────────────────────────────
|
|
477
|
+
"hooks.disabled": ["Stop"],
|
|
478
|
+
// suppress specific hook events; union across layers
|
|
479
|
+
// values: "SessionStart" | "PostToolUseFailure" | "Stop"
|
|
480
|
+
|
|
481
|
+
"hooks.adapter": "claude",
|
|
482
|
+
// explicit adapter when auto-detection is ambiguous
|
|
483
|
+
// values: "claude" | "cursor" | "codex"
|
|
484
|
+
// repo wins over user
|
|
485
|
+
|
|
486
|
+
// ── Telemetry ──────────────────────────────────────────────────────────────
|
|
487
|
+
"telemetry.disabled": true,
|
|
488
|
+
// team-level opt-out for orgs with a no-telemetry policy
|
|
489
|
+
// env LOREKIT_TELEMETRY=0 always wins if set
|
|
490
|
+
|
|
491
|
+
// ── Dedupe threshold ───────────────────────────────────────────────────────
|
|
492
|
+
"dedupe.threshold": 0.8 // Jaccard similarity cutoff for `lorekit dedupe`
|
|
493
|
+
// --threshold flag wins when passed explicitly
|
|
454
494
|
}
|
|
455
495
|
```
|
|
456
496
|
|
|
@@ -488,7 +528,7 @@ active deny constraints.
|
|
|
488
528
|
| `--to <tier>` | Migration destination tier: `home` / `project` (`migrate`; default routes by scope) |
|
|
489
529
|
| `--apply` | Apply the migration — alias of `--yes` (`migrate`) |
|
|
490
530
|
| `-y, --yes` | Non-interactive / apply; never prompt |
|
|
491
|
-
| `--no-hooks` | Skip wiring the lifecycle hooks;
|
|
531
|
+
| `--no-hooks` | Skip wiring the lifecycle hooks; skills + MCP only (`install`) |
|
|
492
532
|
| `--force` | Overwrite existing skill files (`install`) |
|
|
493
533
|
| `--deep` | Write/read/delete round-trip (`doctor`) |
|
|
494
534
|
| `--json` | Machine-readable output (`list` / `search` / `show` / `stats` / `scopes` / `diff` / `tree` / `lint` / `dedupe`) |
|
package/bin/lorekit.mjs
CHANGED
|
@@ -34,7 +34,7 @@ ${c.bold('Usage')}
|
|
|
34
34
|
|
|
35
35
|
${c.bold('Commands')}
|
|
36
36
|
install Scaffold the lorekit-memory + lorekit-setup skills, wire the
|
|
37
|
-
LoreKit MCP server, and install the deterministic hooks (
|
|
37
|
+
LoreKit MCP server, and install the deterministic hooks (memories
|
|
38
38
|
on SessionStart, a nudge on tool failure + at Stop). Prompts to
|
|
39
39
|
install for this project (.claude) or globally for every project
|
|
40
40
|
(~/.claude); --project / --global choose non-interactively,
|
|
@@ -44,39 +44,39 @@ ${c.bold('Commands')}
|
|
|
44
44
|
other servers, hooks, and settings are left untouched. Prompts
|
|
45
45
|
project vs global; --project / --global choose non-interactively.
|
|
46
46
|
doctor Verify the skill install, MCP connectivity, token, and scope.
|
|
47
|
-
list (ls) List the
|
|
47
|
+
list (ls) List the memories that apply to the current directory, split into
|
|
48
48
|
an Offline section (local .lorekit/ + ~/.lorekit/) and a Remote
|
|
49
49
|
section (hosted MCP). Groups by scope (project/branch/repo/global).
|
|
50
50
|
--json for scripting, --scope <s> to narrow.
|
|
51
|
-
search Full-text search the applicable
|
|
51
|
+
search Full-text search the applicable memories across both stores and all
|
|
52
52
|
(grep) scopes (case-insensitive, literal substring over key + value),
|
|
53
53
|
rendered in the same Offline/Remote split. --json, --scope <s>.
|
|
54
|
-
show Inspect one
|
|
54
|
+
show Inspect one memory in full: its complete value, scope, key, updated
|
|
55
55
|
date, tags, and which store(s) it lives in (noting any divergence
|
|
56
56
|
when it is in both). --json. Usage: show <scope> <key>.
|
|
57
|
-
stats Count the applicable
|
|
57
|
+
stats Count the applicable memories per scope and per store (offline vs
|
|
58
58
|
remote), with per-store and grand totals, in the same Offline/
|
|
59
59
|
Remote split. --json, --scope <s>.
|
|
60
|
-
scopes Store-wide inventory of EVERY distinct scope that holds
|
|
61
|
-
with a
|
|
60
|
+
scopes Store-wide inventory of EVERY distinct scope that holds memories,
|
|
61
|
+
with a memory count per scope — not cwd-scoped like the commands
|
|
62
62
|
above (it lists scopes anywhere in the store). Offline is exact;
|
|
63
63
|
the remote can't enumerate scopes (honest note). --json, --scope <s>.
|
|
64
64
|
diff Compare the offline and remote stores for the applicable scopes and
|
|
65
65
|
report divergence: local-only, remote-only, and conflicting keys
|
|
66
66
|
(grouped by scope). Needs both stores readable. --json, --scope <s>.
|
|
67
67
|
tree Show the injected scopes (project → branch → repo → global) as a precedence
|
|
68
|
-
(resolve) hierarchy and mark, per key, which scope's
|
|
68
|
+
(resolve) hierarchy and mark, per key, which scope's memory WINS and which are
|
|
69
69
|
shadowed — the real hook-resolution order. --json, --scope <s>.
|
|
70
|
-
lint Flag low-quality
|
|
70
|
+
lint Flag low-quality memories (empty/short/untrimmed value, empty key,
|
|
71
71
|
malformed scope) across the applicable scopes and both stores. Exits
|
|
72
72
|
non-zero when issues are found (CI gate). --json, --scope <s>.
|
|
73
|
-
dedupe Find likely-duplicate
|
|
73
|
+
dedupe Find likely-duplicate memories via a zero-dep word-overlap HEURISTIC
|
|
74
74
|
(Jaccard >= threshold, not semantic), grouped into clusters per
|
|
75
75
|
store. --json, --scope <s>, --threshold <0..1>.
|
|
76
76
|
migrate Relocate a LoreKit-format local store into the current layout.
|
|
77
77
|
Dry-run by default; pass --yes to apply. Idempotent.
|
|
78
78
|
hook Hook engine for Claude Code / Cursor / Codex. Reads the host's
|
|
79
|
-
JSON on stdin and injects
|
|
79
|
+
JSON on stdin and injects memories or a retrospective nudge.
|
|
80
80
|
Not run by hand — wired into a plugin's hook config.
|
|
81
81
|
mcp Local stdio MCP server. Exposes the memory.* tools backed by the
|
|
82
82
|
resolved store (local .lorekit/ offline, or remote passthrough) so
|
|
@@ -143,7 +143,7 @@ ${c.bold('Usage')}
|
|
|
143
143
|
|
|
144
144
|
Scaffolds the lorekit-memory (runtime read/write) and lorekit-setup (loop
|
|
145
145
|
authoring) skills, adds the LoreKit MCP server, and wires the deterministic
|
|
146
|
-
hooks (
|
|
146
|
+
hooks (memories on SessionStart, a nudge on tool failure + at Stop).
|
|
147
147
|
|
|
148
148
|
${c.bold('Options')}
|
|
149
149
|
-d, --dir <path> Target project root (default: current directory)
|
|
@@ -199,12 +199,12 @@ ${c.bold('Examples')}
|
|
|
199
199
|
npx @lorekit/cli doctor --deep
|
|
200
200
|
npx @lorekit/cli doctor --mode local
|
|
201
201
|
`,
|
|
202
|
-
list: `${c.bold('lorekit list')} — list the
|
|
202
|
+
list: `${c.bold('lorekit list')} — list the memories that apply to the current directory ${c.dim('(alias: ls)')}
|
|
203
203
|
|
|
204
204
|
${c.bold('Usage')}
|
|
205
205
|
npx @lorekit/cli list [options]
|
|
206
206
|
|
|
207
|
-
Shows the
|
|
207
|
+
Shows the memories for the scopes that resolve for the current directory
|
|
208
208
|
(project/branch/repo/global), split into an Offline section (the local
|
|
209
209
|
.lorekit/ + ~/.lorekit/ two-tier store) and a Remote section (the hosted MCP
|
|
210
210
|
server). When no remote token/endpoint is configured the Remote section is a
|
|
@@ -223,14 +223,14 @@ ${c.bold('Examples')}
|
|
|
223
223
|
npx @lorekit/cli list --json
|
|
224
224
|
npx @lorekit/cli list --scope global
|
|
225
225
|
`,
|
|
226
|
-
search: `${c.bold('lorekit search')} — full-text search the applicable
|
|
226
|
+
search: `${c.bold('lorekit search')} — full-text search the applicable memories ${c.dim('(alias: grep)')}
|
|
227
227
|
|
|
228
228
|
${c.bold('Usage')}
|
|
229
229
|
npx @lorekit/cli search <query> [options]
|
|
230
230
|
|
|
231
|
-
Searches every
|
|
231
|
+
Searches every memory for the current directory's scopes (project/branch/repo/
|
|
232
232
|
global) across both stores, matching the query case-insensitively as a LITERAL
|
|
233
|
-
substring of a
|
|
233
|
+
substring of a memory's key or value (regex metacharacters are matched verbatim,
|
|
234
234
|
never interpreted). Results are shown in the same Offline / Remote split as
|
|
235
235
|
\`list\`; an unconfigured remote degrades to a short note, never an error.
|
|
236
236
|
|
|
@@ -247,12 +247,12 @@ ${c.bold('Examples')}
|
|
|
247
247
|
npx @lorekit/cli grep "flaky test" --json
|
|
248
248
|
npx @lorekit/cli search migration --scope global
|
|
249
249
|
`,
|
|
250
|
-
show: `${c.bold('lorekit show')} — inspect one
|
|
250
|
+
show: `${c.bold('lorekit show')} — inspect one memory in full
|
|
251
251
|
|
|
252
252
|
${c.bold('Usage')}
|
|
253
253
|
npx @lorekit/cli show <scope> <key> [options]
|
|
254
254
|
|
|
255
|
-
Prints one
|
|
255
|
+
Prints one memory's complete (untruncated) value, scope, key, updated date, tags,
|
|
256
256
|
and which store(s) it lives in. If the same scope::key exists in both the offline
|
|
257
257
|
and remote stores, both are shown and any divergence in their values is flagged.
|
|
258
258
|
Exits non-zero when the key is found in neither readable store.
|
|
@@ -268,12 +268,12 @@ ${c.bold('Examples')}
|
|
|
268
268
|
npx @lorekit/cli show global prefer-guard-clauses
|
|
269
269
|
npx @lorekit/cli show project::widget build-flags --json
|
|
270
270
|
`,
|
|
271
|
-
stats: `${c.bold('lorekit stats')} — count the applicable
|
|
271
|
+
stats: `${c.bold('lorekit stats')} — count the applicable memories per scope and per store
|
|
272
272
|
|
|
273
273
|
${c.bold('Usage')}
|
|
274
274
|
npx @lorekit/cli stats [options]
|
|
275
275
|
|
|
276
|
-
Shows how many
|
|
276
|
+
Shows how many memories apply to the current directory's scopes (project/branch/
|
|
277
277
|
repo/global), broken down per scope and per store (Offline = the local .lorekit/
|
|
278
278
|
+ ~/.lorekit/ two-tier store; Remote = the hosted MCP server), with per-store and
|
|
279
279
|
grand totals. An unconfigured remote degrades to a short note, never an error.
|
|
@@ -296,13 +296,13 @@ ${c.bold('Examples')}
|
|
|
296
296
|
${c.bold('Usage')}
|
|
297
297
|
npx @lorekit/cli scopes [options]
|
|
298
298
|
|
|
299
|
-
Lists EVERY distinct scope present in the store, with a
|
|
299
|
+
Lists EVERY distinct scope present in the store, with a memory count per scope,
|
|
300
300
|
in the same Offline / Remote split as the other read commands. Unlike \`list\` /
|
|
301
301
|
\`stats\` (which only see the scopes that resolve for the current directory), this
|
|
302
302
|
is a full inventory — it surfaces scopes anywhere in the store, regardless of the
|
|
303
303
|
current directory.
|
|
304
304
|
|
|
305
|
-
Offline counts are exact: each scope is read from the
|
|
305
|
+
Offline counts are exact: each scope is read from the memory files' frontmatter,
|
|
306
306
|
not reverse-mapped from the directory layout. The Remote section is always a
|
|
307
307
|
short note: the hosted MCP surface has no "list all scopes" tool (every read tool
|
|
308
308
|
requires a scope), so a remote inventory isn't possible — never an error (exit 0).
|
|
@@ -344,16 +344,16 @@ ${c.bold('Examples')}
|
|
|
344
344
|
npx @lorekit/cli diff --json
|
|
345
345
|
npx @lorekit/cli diff --scope global
|
|
346
346
|
`,
|
|
347
|
-
tree: `${c.bold('lorekit tree')} — show the scope precedence hierarchy and which
|
|
347
|
+
tree: `${c.bold('lorekit tree')} — show the scope precedence hierarchy and which memory wins ${c.dim('(alias: resolve)')}
|
|
348
348
|
|
|
349
349
|
${c.bold('Usage')}
|
|
350
350
|
npx @lorekit/cli tree [options]
|
|
351
351
|
|
|
352
352
|
Shows the scopes the hooks actually inject for the current directory — project,
|
|
353
353
|
branch, repo, global, in precedence order (most-specific first) — and marks, for any key
|
|
354
|
-
present at more than one scope, which scope's
|
|
354
|
+
present at more than one scope, which scope's memory WINS and which are shadowed.
|
|
355
355
|
This mirrors the SessionStart hook's resolution exactly (a more-specific scope
|
|
356
|
-
overrides a broader scope's same-key
|
|
356
|
+
overrides a broader scope's same-key memory). Resolved independently per store,
|
|
357
357
|
in the same Offline / Remote split.
|
|
358
358
|
|
|
359
359
|
${c.bold('Options')}
|
|
@@ -368,12 +368,12 @@ ${c.bold('Examples')}
|
|
|
368
368
|
npx @lorekit/cli tree
|
|
369
369
|
npx @lorekit/cli resolve --json
|
|
370
370
|
`,
|
|
371
|
-
lint: `${c.bold('lorekit lint')} — flag low-quality
|
|
371
|
+
lint: `${c.bold('lorekit lint')} — flag low-quality memories across the applicable scopes
|
|
372
372
|
|
|
373
373
|
${c.bold('Usage')}
|
|
374
374
|
npx @lorekit/cli lint [options]
|
|
375
375
|
|
|
376
|
-
Checks every
|
|
376
|
+
Checks every memory for the current directory's scopes (project/branch/repo/
|
|
377
377
|
global), across both stores, against a small set of quality rules: empty or
|
|
378
378
|
whitespace-only value, suspiciously short value, untrimmed value, empty key, and
|
|
379
379
|
malformed scope (e.g. a single \`:\` where \`::\` is expected). Each finding names
|
|
@@ -393,12 +393,12 @@ ${c.bold('Examples')}
|
|
|
393
393
|
npx @lorekit/cli lint --json
|
|
394
394
|
npx @lorekit/cli lint --scope global
|
|
395
395
|
`,
|
|
396
|
-
dedupe: `${c.bold('lorekit dedupe')} — find likely-duplicate
|
|
396
|
+
dedupe: `${c.bold('lorekit dedupe')} — find likely-duplicate memories (heuristic)
|
|
397
397
|
|
|
398
398
|
${c.bold('Usage')}
|
|
399
399
|
npx @lorekit/cli dedupe [options]
|
|
400
400
|
|
|
401
|
-
Groups
|
|
401
|
+
Groups memories whose values overlap heavily into duplicate clusters, per store,
|
|
402
402
|
across the current directory's scopes. The similarity signal is a zero-dependency
|
|
403
403
|
HEURISTIC — Jaccard overlap of lowercased word tokens, not a semantic/embedding
|
|
404
404
|
measure — so it surfaces candidates for a human to review, and can both miss
|
|
@@ -440,7 +440,7 @@ ${c.bold('Examples')}
|
|
|
440
440
|
${c.bold('Usage')}
|
|
441
441
|
lorekit hook --adapter <claude|cursor|codex> --event <name> [--dir <path>]
|
|
442
442
|
|
|
443
|
-
Machine-facing: reads the host's JSON on stdin and injects
|
|
443
|
+
Machine-facing: reads the host's JSON on stdin and injects memories or a
|
|
444
444
|
retrospective nudge on stdout, always exiting 0. Wired into a plugin's hook
|
|
445
445
|
config by \`lorekit install\` — not run by hand.
|
|
446
446
|
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -345,8 +345,9 @@ export function tokenKind(token) {
|
|
|
345
345
|
|
|
346
346
|
// For hooks: resolve the connection closest-scope first — the project's
|
|
347
347
|
// .mcp.json (a project install), then the global ~/.claude.json (a global
|
|
348
|
-
// install), then
|
|
349
|
-
//
|
|
348
|
+
// install), then the `mcp.endpoint` field in .lorekit.json (committable URL
|
|
349
|
+
// without token — safe for VCS), then env. `splitEndpoint` is passed in to
|
|
350
|
+
// avoid a circular import with mcp.mjs.
|
|
350
351
|
export function resolveProjectConnection(root, splitEndpoint) {
|
|
351
352
|
const sources = [readLorekitServer(root), readServerFromFile(mcpConfigPath(root, 'global'))];
|
|
352
353
|
for (const configured of sources) {
|
|
@@ -360,5 +361,33 @@ export function resolveProjectConnection(root, splitEndpoint) {
|
|
|
360
361
|
}
|
|
361
362
|
}
|
|
362
363
|
}
|
|
364
|
+
|
|
365
|
+
// Fallback: `mcp.endpoint` in .lorekit.json — a committable URL without token.
|
|
366
|
+
// Token still comes from .mcp.json or LOREKIT_TOKEN.
|
|
367
|
+
const lorekitJson = readLorekitJson(root);
|
|
368
|
+
const committedEndpoint =
|
|
369
|
+
lorekitJson && typeof lorekitJson['mcp.endpoint'] === 'string'
|
|
370
|
+
? lorekitJson['mcp.endpoint'].trim()
|
|
371
|
+
: null;
|
|
372
|
+
if (committedEndpoint && !committedEndpoint.includes('<project-ref>')) {
|
|
373
|
+
return {
|
|
374
|
+
endpoint: committedEndpoint,
|
|
375
|
+
token: process.env.LOREKIT_TOKEN || null,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
363
379
|
return resolveConnection({});
|
|
364
380
|
}
|
|
381
|
+
|
|
382
|
+
// Read .lorekit.json from the repo root without throwing. Exported so other
|
|
383
|
+
// CLI modules can read per-repo config without duplicating the same try/catch.
|
|
384
|
+
// control.mjs uses its own internal readJson(file) for historical reasons and
|
|
385
|
+
// does not import this — that is a known duplication, not a bug.
|
|
386
|
+
export function readLorekitJson(root) {
|
|
387
|
+
const file = path.join(root, '.lorekit.json');
|
|
388
|
+
try {
|
|
389
|
+
return JSON.parse(fs.readFileSync(file, 'utf8')) || {};
|
|
390
|
+
} catch {
|
|
391
|
+
return {};
|
|
392
|
+
}
|
|
393
|
+
}
|
package/src/control.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// The control model: decide the memory mode (off | local | remote), the store
|
|
2
|
-
// target, who decided, and which deny constraints are active.
|
|
2
|
+
// target, who decided, and which deny constraints are active. Also resolves
|
|
3
|
+
// write-behaviour properties from the config layers:
|
|
4
|
+
//
|
|
5
|
+
// scope.defaults — map of scope-prefix → { tags } applied to every matching write
|
|
6
|
+
// tags.default — array of tags appended to every write (both layers merged)
|
|
7
|
+
// hooks.disabled — array of hook event names to suppress (e.g. ["Stop"])
|
|
8
|
+
// hooks.adapter — explicit adapter override ("claude" | "cursor" | "codex")
|
|
3
9
|
//
|
|
4
10
|
// Two layers of config, two kinds of statement:
|
|
5
11
|
// - a SELECT (`mode`) chooses a mode within what is allowed;
|
|
@@ -104,7 +110,44 @@ export function resolveControl({
|
|
|
104
110
|
storeTarget = connection.endpoint || null;
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
// 5. Write-behaviour properties resolved from config layers.
|
|
114
|
+
// `tags.default` — both layers merged, user supplements repo (no override).
|
|
115
|
+
const tagsDefault = [
|
|
116
|
+
...asList(repoConfig['tags.default']),
|
|
117
|
+
...asList(userConfig['tags.default']),
|
|
118
|
+
].filter((t) => typeof t === 'string' && t.length > 0);
|
|
119
|
+
|
|
120
|
+
// `scope.defaults` — repo layer only (team-scoped write policy).
|
|
121
|
+
// Schema: { "<scope-prefix>": { "tags": [...] } }
|
|
122
|
+
// Matched against a write's resolved scope using startsWith — no glob dep.
|
|
123
|
+
const scopeDefaults =
|
|
124
|
+
repoConfig['scope.defaults'] && typeof repoConfig['scope.defaults'] === 'object'
|
|
125
|
+
? repoConfig['scope.defaults']
|
|
126
|
+
: null;
|
|
127
|
+
|
|
128
|
+
// `hooks.disabled` — union of both layers (either layer can suppress an event).
|
|
129
|
+
const hooksDisabled = new Set([
|
|
130
|
+
...asList(repoConfig['hooks.disabled']),
|
|
131
|
+
...asList(userConfig['hooks.disabled']),
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// `hooks.adapter` — repo layer wins over user layer (explicit project override).
|
|
135
|
+
const hooksAdapter =
|
|
136
|
+
(typeof repoConfig['hooks.adapter'] === 'string' && repoConfig['hooks.adapter'].trim()) ||
|
|
137
|
+
(typeof userConfig['hooks.adapter'] === 'string' && userConfig['hooks.adapter'].trim()) ||
|
|
138
|
+
null;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
mode: chosen.mode,
|
|
142
|
+
storeTarget,
|
|
143
|
+
decidedBy,
|
|
144
|
+
denies,
|
|
145
|
+
connection,
|
|
146
|
+
tagsDefault,
|
|
147
|
+
scopeDefaults,
|
|
148
|
+
hooksDisabled,
|
|
149
|
+
hooksAdapter,
|
|
150
|
+
};
|
|
108
151
|
}
|
|
109
152
|
|
|
110
153
|
// The per-repo project-tier directory: $LOREKIT_STORE, else a `store` override
|
package/src/core/lessons.mjs
CHANGED
|
@@ -55,7 +55,7 @@ export async function fetchLessons(store, cwd) {
|
|
|
55
55
|
export function formatLessons(lessons, scope) {
|
|
56
56
|
if (!lessons || lessons.length === 0) return null;
|
|
57
57
|
const header =
|
|
58
|
-
`LoreKit — ${lessons.length} shared
|
|
58
|
+
`LoreKit — ${lessons.length} shared ${lessons.length === 1 ? 'memory' : 'memories'} for ${scope.repoScope || 'this workspace'}. ` +
|
|
59
59
|
`Treat as considerations, not rules; trust the current code if they conflict.`;
|
|
60
60
|
const body = lessons
|
|
61
61
|
.map((l) => {
|
|
@@ -110,7 +110,7 @@ export function formatRelevantLessons(lessons) {
|
|
|
110
110
|
if (!lessons || lessons.length === 0) return null;
|
|
111
111
|
const header =
|
|
112
112
|
`LoreKit — you've hit something like this before. ${lessons.length} related ` +
|
|
113
|
-
'
|
|
113
|
+
`${lessons.length === 1 ? 'memory' : 'memories'} (considerations, not rules; trust the current code if they conflict):`;
|
|
114
114
|
const body = lessons
|
|
115
115
|
.map((l) => {
|
|
116
116
|
const first = String(l.value || '').split('\n')[0].slice(0, 300);
|
|
@@ -120,25 +120,52 @@ export function formatRelevantLessons(lessons) {
|
|
|
120
120
|
return `${header}\n${body}`;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Build a tags hint string from config-resolved tags and scope defaults. Returns
|
|
124
|
+
// "" when there are no configured tags (no hint appended to the nudge).
|
|
125
|
+
function tagsHint(writeScope, { tagsDefault = [], scopeDefaults = null } = {}) {
|
|
126
|
+
const tags = [...tagsDefault];
|
|
127
|
+
if (scopeDefaults) {
|
|
128
|
+
for (const [prefix, cfg] of Object.entries(scopeDefaults)) {
|
|
129
|
+
if (
|
|
130
|
+
writeScope === prefix ||
|
|
131
|
+
writeScope.startsWith(prefix.endsWith('::') ? prefix : prefix + '::')
|
|
132
|
+
) {
|
|
133
|
+
for (const t of Array.isArray(cfg.tags) ? cfg.tags : []) {
|
|
134
|
+
if (typeof t === 'string' && t.length > 0 && !tags.includes(t)) tags.push(t);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (tags.length === 0) return '';
|
|
140
|
+
return ` Include tags: [${tags.map((t) => JSON.stringify(t)).join(', ')}].`;
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
// The retrospective nudge emitted at end-of-turn (one-shot per session).
|
|
124
|
-
|
|
144
|
+
// `control` is the resolved control object (optional) — carries tagsDefault and
|
|
145
|
+
// scopeDefaults when the repo/user config defines them.
|
|
146
|
+
export function retrospectiveNudge(scope, control) {
|
|
125
147
|
const writeScope = scope.repoScope || 'global';
|
|
148
|
+
const hint = tagsHint(writeScope, control);
|
|
126
149
|
return (
|
|
127
150
|
'LoreKit retrospective: if this session hit a stuck loop, a repeated ' +
|
|
128
151
|
'command failure, a surprising gotcha, a near-miss, or a wrong assumption ' +
|
|
129
152
|
'that cost time, record it now via the lorekit-memory skill ' +
|
|
130
|
-
`(memory.write to ${writeScope}, phrased as an observation)
|
|
153
|
+
`(memory.write to ${writeScope}, phrased as an observation).${hint} ` +
|
|
131
154
|
'If nothing was durable, do nothing.'
|
|
132
155
|
);
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
// The nudge emitted when a tool failure is detected.
|
|
136
|
-
|
|
159
|
+
// `control` is the resolved control object (optional) — carries tagsDefault and
|
|
160
|
+
// scopeDefaults when the repo/user config defines them.
|
|
161
|
+
export function failureNudge(toolName, scope, control) {
|
|
137
162
|
const writeScope = scope.repoScope || 'global';
|
|
163
|
+
const hint = tagsHint(writeScope, control);
|
|
164
|
+
const suffix = hint ? `${hint} So the next run avoids it.` : 'so the next run avoids it.';
|
|
138
165
|
return (
|
|
139
166
|
`LoreKit: the last ${toolName} call failed. If this is a recurring or ` +
|
|
140
|
-
'non-obvious failure, consider recording the fix as a
|
|
141
|
-
`lorekit-memory (memory.write to ${writeScope})
|
|
167
|
+
'non-obvious failure, consider recording the fix as a memory via ' +
|
|
168
|
+
`lorekit-memory (memory.write to ${writeScope}) — ${suffix}`
|
|
142
169
|
);
|
|
143
170
|
}
|
|
144
171
|
|
package/src/dedupe.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Same Offline / Remote split and graceful degradation as `list`. Read-only.
|
|
12
12
|
// Human-facing, so the bin wraps it in `traceCommand`.
|
|
13
13
|
import process from 'node:process';
|
|
14
|
-
import { resolveProjectRoot } from './config.mjs';
|
|
14
|
+
import { resolveProjectRoot, readLorekitJson } from './config.mjs';
|
|
15
15
|
import { deriveScope } from './scope.mjs';
|
|
16
16
|
import { resolveDenies } from './control.mjs';
|
|
17
17
|
import { resolveStores, remoteUnavailableReason } from './stores.mjs';
|
|
@@ -29,6 +29,16 @@ export function parseThreshold(raw) {
|
|
|
29
29
|
return Math.min(1, Math.max(0, n));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Read `dedupe.threshold` from .lorekit.json (non-throwing). Returns the
|
|
33
|
+
// repo-default threshold, or undefined when not configured.
|
|
34
|
+
export function repoThreshold(root) {
|
|
35
|
+
const cfg = readLorekitJson(root);
|
|
36
|
+
if (cfg['dedupe.threshold'] !== undefined) {
|
|
37
|
+
return parseThreshold(cfg['dedupe.threshold']);
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
// Flatten a `gather()` result into one entry list (each entry keeps its scope)
|
|
33
43
|
// for cross-scope clustering, plus the scopes whose read errored (they can't be
|
|
34
44
|
// clustered and are surfaced, not silently dropped).
|
|
@@ -50,7 +60,11 @@ export async function dedupe(args) {
|
|
|
50
60
|
const env = { ...process.env };
|
|
51
61
|
if (args.store) env.LOREKIT_STORE = args.store;
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
// Threshold precedence: --threshold flag > dedupe.threshold in .lorekit.json > default (0.8).
|
|
64
|
+
const threshold =
|
|
65
|
+
args.threshold !== undefined
|
|
66
|
+
? parseThreshold(args.threshold)
|
|
67
|
+
: (repoThreshold(root) ?? DEFAULT_THRESHOLD);
|
|
54
68
|
const scopeInfo = deriveScope(root);
|
|
55
69
|
// Default to every applicable scope; `--scope <s>` narrows to one.
|
|
56
70
|
const scopes = args.scope && typeof args.scope === 'string' ? [args.scope] : scopeList(scopeInfo);
|
|
@@ -152,7 +166,7 @@ function renderDedupeSection(header, section) {
|
|
|
152
166
|
cluster.minSimilarity === cluster.maxSimilarity
|
|
153
167
|
? cluster.minSimilarity.toFixed(2)
|
|
154
168
|
: `${cluster.minSimilarity.toFixed(2)}–${cluster.maxSimilarity.toFixed(2)}`;
|
|
155
|
-
log(` ${c.yellow('•')} cluster ${n} ${c.dim(`(${cluster.size}
|
|
169
|
+
log(` ${c.yellow('•')} cluster ${n} ${c.dim(`(${cluster.size} memories, similarity ${range})`)}`);
|
|
156
170
|
for (const m of cluster.members) {
|
|
157
171
|
log(` ${c.cyan('-')} ${m.scope}::${m.key}`);
|
|
158
172
|
}
|
package/src/doctor.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
readLorekitServer,
|
|
12
12
|
readMcpConfig,
|
|
13
13
|
tokenKind,
|
|
14
|
+
readLorekitJson,
|
|
14
15
|
} from './config.mjs';
|
|
15
16
|
import { splitEndpoint } from './mcp.mjs';
|
|
16
17
|
import { deriveScope } from './scope.mjs';
|
|
@@ -82,9 +83,24 @@ export async function doctor(args) {
|
|
|
82
83
|
const scope = deriveScope(root);
|
|
83
84
|
if (scope.hasRemote) {
|
|
84
85
|
record('info', 'read scope', scope.readOrder.join(' → '));
|
|
85
|
-
record('info', 'write scope', `${scope.repoScope} (default for "went wrong"
|
|
86
|
+
record('info', 'write scope', `${scope.repoScope} (default for "went wrong" memories)`);
|
|
86
87
|
} else {
|
|
87
|
-
record('warn', 'scope', 'no git remote here —
|
|
88
|
+
record('warn', 'scope', 'no git remote here — memories fall back to global');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 6. doctor.require — committed list of checks that MUST pass.
|
|
92
|
+
// Useful as a CI gate: any check in the list that did not pass causes a failure.
|
|
93
|
+
const lorekitJson = readLorekitJson(root);
|
|
94
|
+
const required = (Array.isArray(lorekitJson['doctor.require']) ? lorekitJson['doctor.require'] : [])
|
|
95
|
+
.filter((r) => typeof r === 'string');
|
|
96
|
+
for (const req of required) {
|
|
97
|
+
// A required check passes if its label is NOT in failedChecks (it either
|
|
98
|
+
// passed or was never run; unknown labels get a pass to avoid false failures).
|
|
99
|
+
if (failedChecks.includes(req)) {
|
|
100
|
+
record('fail', 'doctor.require', `required check failed: ${req}`);
|
|
101
|
+
} else {
|
|
102
|
+
record('pass', 'doctor.require', `required check passed: ${req}`);
|
|
103
|
+
}
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
// Summary.
|
|
@@ -128,7 +144,8 @@ async function checkLocal(control, root, args, record) {
|
|
|
128
144
|
|
|
129
145
|
// Home tier — per-user, cross-repo, always available.
|
|
130
146
|
record('pass', 'home store', prettyPath(store.homeDir));
|
|
131
|
-
|
|
147
|
+
const homeCount = await store.home.count(scopes);
|
|
148
|
+
record('info', 'home entries', `${homeCount} ${homeCount === 1 ? 'memory' : 'memories'}`);
|
|
132
149
|
|
|
133
150
|
// Project tier — opt-in; active only when its directory exists.
|
|
134
151
|
const projRel = path.relative(root, store.projectDir) || store.projectDir;
|
|
@@ -137,12 +154,13 @@ async function checkLocal(control, root, args, record) {
|
|
|
137
154
|
? 'committed — shared with the team'
|
|
138
155
|
: 'gitignored — private to your checkout';
|
|
139
156
|
record('pass', 'project store', projRel);
|
|
140
|
-
|
|
157
|
+
const projCount = await store.project.count(scopes);
|
|
158
|
+
record('info', 'project entries', `${projCount} ${projCount === 1 ? 'memory' : 'memories'} — ${sharing}`);
|
|
141
159
|
} else {
|
|
142
160
|
record(
|
|
143
161
|
'info',
|
|
144
162
|
'project store',
|
|
145
|
-
`${projRel} — not opted-in (create it to persist repo/branch
|
|
163
|
+
`${projRel} — not opted-in (create it to persist repo/branch memories here)`,
|
|
146
164
|
);
|
|
147
165
|
}
|
|
148
166
|
|
package/src/hook.mjs
CHANGED
|
@@ -83,6 +83,9 @@ async function run(args) {
|
|
|
83
83
|
const control = loadControl(root);
|
|
84
84
|
if (control.mode === 'off') return 0;
|
|
85
85
|
|
|
86
|
+
// `hooks.disabled` — skip the event if this event name is suppressed by config.
|
|
87
|
+
if (control.hooksDisabled && control.hooksDisabled.has(event)) return 0;
|
|
88
|
+
|
|
86
89
|
const emit = (text) => {
|
|
87
90
|
if (text) process.stdout.write(adapter.emit(event, text));
|
|
88
91
|
};
|
|
@@ -114,14 +117,14 @@ async function run(args) {
|
|
|
114
117
|
} catch {
|
|
115
118
|
relevant = null; // never let a lesson lookup break the failure nudge
|
|
116
119
|
}
|
|
117
|
-
const nudge = failureNudge(parsed.toolName, scope);
|
|
120
|
+
const nudge = failureNudge(parsed.toolName, scope, control);
|
|
118
121
|
emit(relevant ? `${relevant}\n\n${nudge}` : nudge);
|
|
119
122
|
return 0;
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
if (intent === 'retrospective') {
|
|
123
126
|
if (!firstTimeThisSession(parsed.sessionId, 'retro')) return 0;
|
|
124
|
-
emit(retrospectiveNudge(scope));
|
|
127
|
+
emit(retrospectiveNudge(scope, control));
|
|
125
128
|
return 0;
|
|
126
129
|
}
|
|
127
130
|
|
package/src/install.mjs
CHANGED
|
@@ -113,7 +113,7 @@ export async function install(args) {
|
|
|
113
113
|
status('pass', mcpLabel, `${existed ? 'updated' : 'created'} lorekit server → ${display(file)}`);
|
|
114
114
|
|
|
115
115
|
if (!wireHooks) {
|
|
116
|
-
status('info', 'hooks', 'skipped (--no-hooks) — the skill still works, but
|
|
116
|
+
status('info', 'hooks', 'skipped (--no-hooks) — the skill still works, but memories are model-invoked only');
|
|
117
117
|
} else {
|
|
118
118
|
const n = hooks.added + hooks.updated;
|
|
119
119
|
const hookState =
|
|
@@ -127,9 +127,9 @@ export async function install(args) {
|
|
|
127
127
|
if (kind === 'none') {
|
|
128
128
|
status('warn', 'token', 'none configured — reads/writes will fail until a token is set');
|
|
129
129
|
} else if (kind === 'read-only') {
|
|
130
|
-
status('warn', 'token', 'read-only (lk_ro_*) — the skill can read
|
|
130
|
+
status('warn', 'token', 'read-only (lk_ro_*) — the skill can read memories but not write them');
|
|
131
131
|
} else if (kind === 'write-only') {
|
|
132
|
-
status('warn', 'token', 'write-only (lk_wo_*) — the skill can write
|
|
132
|
+
status('warn', 'token', 'write-only (lk_wo_*) — the skill can write memories but not read them');
|
|
133
133
|
} else if (kind === 'unknown') {
|
|
134
134
|
status('warn', 'token', 'unrecognized prefix — expected lk_rw_*, lk_ro_*, or lk_wo_*');
|
|
135
135
|
} else {
|
|
@@ -140,7 +140,7 @@ export async function install(args) {
|
|
|
140
140
|
if (gitScope.hasRemote) {
|
|
141
141
|
status('info', 'scope', `${gitScope.repoScope}${gitScope.branchScope ? ` · ${gitScope.branchScope}` : ''}`);
|
|
142
142
|
} else {
|
|
143
|
-
status('warn', 'scope', 'no git remote —
|
|
143
|
+
status('warn', 'scope', 'no git remote — memories will fall back to global');
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
log(`\n Next: ${c.cyan('npx @lorekit/cli doctor')} to verify the connection.`);
|
package/src/lessons-view.mjs
CHANGED
|
@@ -429,9 +429,9 @@ export function renderSection(header, section) {
|
|
|
429
429
|
|
|
430
430
|
const printable = (section.groups || []).filter((g) => g.entries.length || g.error);
|
|
431
431
|
if (!printable.length) {
|
|
432
|
-
// `header.empty` lets `search` say "no
|
|
433
|
-
// "no
|
|
434
|
-
log(` ${c.dim(header.empty || 'no
|
|
432
|
+
// `header.empty` lets `search` say "no memories match" where `list` says
|
|
433
|
+
// "no memories found"; both are the same empty-section case.
|
|
434
|
+
log(` ${c.dim(header.empty || 'no memories found in the applicable scopes')}`);
|
|
435
435
|
return;
|
|
436
436
|
}
|
|
437
437
|
|
package/src/list.mjs
CHANGED
|
@@ -70,7 +70,7 @@ export async function list(args) {
|
|
|
70
70
|
if (args.json) {
|
|
71
71
|
log(JSON.stringify(buildJson({ root, scopes, offlineSection, remoteSection }), null, 2));
|
|
72
72
|
} else {
|
|
73
|
-
heading('LoreKit
|
|
73
|
+
heading('LoreKit memories');
|
|
74
74
|
log(` project: ${c.dim(root)}`);
|
|
75
75
|
log(` scopes: ${scopes.join(' → ')}`);
|
|
76
76
|
|
package/src/mcp-server.mjs
CHANGED
|
@@ -36,7 +36,7 @@ const SERVER_INFO = { name: 'lorekit-local', version: '1.0.0' };
|
|
|
36
36
|
export const MEMORY_TOOL_DEFS = [
|
|
37
37
|
{
|
|
38
38
|
name: 'memory.write',
|
|
39
|
-
description: 'Store or update a
|
|
39
|
+
description: 'Store or update a memory',
|
|
40
40
|
inputSchema: {
|
|
41
41
|
type: 'object',
|
|
42
42
|
required: ['scope', 'key', 'value'],
|
|
@@ -58,24 +58,24 @@ export const MEMORY_TOOL_DEFS = [
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
name: 'memory.read',
|
|
61
|
-
description: 'Read a
|
|
61
|
+
description: 'Read a memory by scope and key',
|
|
62
62
|
inputSchema: { type: 'object', required: ['scope', 'key'] },
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
name: 'memory.list',
|
|
66
|
-
description: 'List
|
|
66
|
+
description: 'List memories for a scope',
|
|
67
67
|
inputSchema: { type: 'object', required: ['scope'] },
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
name: 'memory.search',
|
|
71
|
-
description: 'Keyword search across
|
|
71
|
+
description: 'Keyword search across memories',
|
|
72
72
|
inputSchema: { type: 'object', required: ['q'] },
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
name: 'memory.delete',
|
|
76
76
|
description:
|
|
77
|
-
'Soft-archive a
|
|
78
|
-
'Archived
|
|
77
|
+
'Soft-archive a memory (default) or hard-delete it (force: true). ' +
|
|
78
|
+
'Archived memories are hidden from reads but can be restored.',
|
|
79
79
|
inputSchema: {
|
|
80
80
|
type: 'object',
|
|
81
81
|
required: ['scope', 'key'],
|
|
@@ -88,7 +88,7 @@ export const MEMORY_TOOL_DEFS = [
|
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
name: 'memory.archive',
|
|
91
|
-
description: 'Soft-archive a
|
|
91
|
+
description: 'Soft-archive a memory. Hidden from reads but restorable.',
|
|
92
92
|
inputSchema: { type: 'object', required: ['scope', 'key'] },
|
|
93
93
|
},
|
|
94
94
|
];
|
package/src/scopes.mjs
CHANGED
|
@@ -113,7 +113,7 @@ function renderScopesSection(header, section) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
if (!section.scopes.length) {
|
|
116
|
-
log(` ${c.dim('no scopes found — the store holds no
|
|
116
|
+
log(` ${c.dim('no scopes found — the store holds no memories')}`);
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
119
|
|
package/src/search.mjs
CHANGED
|
@@ -77,7 +77,7 @@ export async function search(args) {
|
|
|
77
77
|
log(` query: ${c.dim(query)}`);
|
|
78
78
|
log(` scopes: ${scopes.join(' → ')}`);
|
|
79
79
|
|
|
80
|
-
const empty = 'no
|
|
80
|
+
const empty = 'no memories match';
|
|
81
81
|
renderSection({ title: 'Offline', empty }, offlineSection);
|
|
82
82
|
renderSection(
|
|
83
83
|
{ title: 'Remote', subtitle: remoteAvailable ? connection.endpoint : undefined, empty },
|
package/src/show.mjs
CHANGED
|
@@ -78,7 +78,7 @@ export async function show(args) {
|
|
|
78
78
|
if (args.json) {
|
|
79
79
|
log(JSON.stringify(buildJson({ scope, key, offline, remote_, diverged }), null, 2));
|
|
80
80
|
} else {
|
|
81
|
-
heading('LoreKit
|
|
81
|
+
heading('LoreKit memory');
|
|
82
82
|
log(` scope: ${c.dim(scope)}`);
|
|
83
83
|
log(` key: ${c.dim(key)}`);
|
|
84
84
|
|
|
@@ -95,7 +95,7 @@ export async function show(args) {
|
|
|
95
95
|
}
|
|
96
96
|
if (!found) {
|
|
97
97
|
log('');
|
|
98
|
-
log(` ${c.dim(`no
|
|
98
|
+
log(` ${c.dim(`no memory found for ${scope}::${key} in the readable store(s)`)}`);
|
|
99
99
|
}
|
|
100
100
|
log('');
|
|
101
101
|
}
|
package/src/telemetry.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import process from 'node:process';
|
|
27
27
|
import { TELEMETRY_TOKEN } from './telemetry-token.mjs';
|
|
28
|
+
import { readLorekitJson } from './config.mjs';
|
|
28
29
|
|
|
29
30
|
// ── Baked-in defaults (public by design) ──────────────────────────────────────
|
|
30
31
|
// The endpoint is a committed default; the token is injected at publish time
|
|
@@ -42,11 +43,13 @@ const OFF_VALUES = new Set(['0', 'off', 'false', 'no', 'disable', 'disabled']);
|
|
|
42
43
|
// ── Config resolution ─────────────────────────────────────────────────────────
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
|
-
* Resolve telemetry config from env + baked-in defaults.
|
|
46
|
+
* Resolve telemetry config from env + baked-in defaults + .lorekit.json.
|
|
46
47
|
* Returns { enabled: false } when disabled or unconfigured, else the endpoint
|
|
47
48
|
* and headers to export with.
|
|
49
|
+
* @param {object} [env] defaults to process.env
|
|
50
|
+
* @param {object} [repoConfig] pre-loaded .lorekit.json (optional; read from cwd if absent)
|
|
48
51
|
*/
|
|
49
|
-
export function resolveTelemetryConfig(env = process.env) {
|
|
52
|
+
export function resolveTelemetryConfig(env = process.env, repoConfig) {
|
|
50
53
|
const optOut = env.LOREKIT_TELEMETRY;
|
|
51
54
|
if (optOut !== undefined && OFF_VALUES.has(String(optOut).trim().toLowerCase())) {
|
|
52
55
|
return { enabled: false };
|
|
@@ -57,6 +60,12 @@ export function resolveTelemetryConfig(env = process.env) {
|
|
|
57
60
|
if (env.DO_NOT_TRACK && String(env.DO_NOT_TRACK).trim() === '1') {
|
|
58
61
|
return { enabled: false };
|
|
59
62
|
}
|
|
63
|
+
// `telemetry.disabled: true` in .lorekit.json — team-level opt-out committed
|
|
64
|
+
// to the repo. Checked after env overrides (env always wins).
|
|
65
|
+
const cfg = repoConfig !== undefined ? repoConfig : readLorekitJson(process.cwd());
|
|
66
|
+
if (cfg['telemetry.disabled'] === true) {
|
|
67
|
+
return { enabled: false };
|
|
68
|
+
}
|
|
60
69
|
|
|
61
70
|
const endpoint = (env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_ENDPOINT || '')
|
|
62
71
|
.trim()
|
package/src/tree.mjs
CHANGED
|
@@ -107,7 +107,7 @@ function renderTreeSection(header, section) {
|
|
|
107
107
|
|
|
108
108
|
const printable = (section.groups || []).filter((g) => g.entries.length || g.error);
|
|
109
109
|
if (!printable.length) {
|
|
110
|
-
log(` ${c.dim('no
|
|
110
|
+
log(` ${c.dim('no memories found in the applicable scopes')}`);
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
|