@lorekit/cli 1.63.2 → 1.64.1

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
@@ -419,11 +419,17 @@ body below a small length threshold), **untrimmed-value** (real content with
419
419
  surrounding whitespace), **empty-key** (blank key), **volatile-key** (the key
420
420
  carries a per-sighting identifier — a run of 6+ digits such as a GitHub comment
421
421
  id, or a `pr<n>` / `issue<n>` segment — so it never collides, never dedups, and
422
- freezes `seen_count` at 1), and **malformed-scope** (e.g.
423
- a single `:` where `::` is expected). `lint` **exits non-zero (1) when any issue
424
- is found**, so it is usable as a CI gate (`lorekit lint || exit 1`); a clean run —
425
- or one where only a store is unavailable exits 0. The pure rule predicates live
426
- in `lessons-view.mjs` and are unit-tested one rule at a time.
422
+ freezes `seen_count` at 1), **malformed-scope** (e.g.
423
+ a single `:` where `::` is expected), and **unkinded-state-record** (the value
424
+ parses as a JSON object/array but `kind` is unset bare scalars are excluded,
425
+ caught by `short-value` only when short; a longer bare scalar is judged by no
426
+ rule because the SessionStart digest excludes non-lesson kinds by *kind*,
427
+ not by shape, so an un-kinded state record renders as a raw JSON blob inline
428
+ in every session's digest; set `--kind bus`/`--kind signal`).
429
+ `lint` **exits non-zero (1) when any issue is found**, so it is usable as a CI
430
+ gate (`lorekit lint || exit 1`); a clean run — or one where only a store is
431
+ unavailable — exits 0. The pure rule predicates live in `lessons-view.mjs` and
432
+ are unit-tested one rule at a time.
427
433
 
428
434
  ### `lorekit dedupe`
429
435
 
@@ -703,7 +709,9 @@ loops: a per-user **home** tier plus an opt-in per-repo **project** tier.
703
709
 
704
710
  Each tier is foldered by canonical scope, one markdown file per lesson, with
705
711
  YAML frontmatter (`scope, key, tags, source_agent, trigger, created, updated,
706
- archived_at`) and the lesson as the body:
712
+ archived_at, origin_repo, origin_branch, origin_commit, origin_pr,
713
+ expires_at, seen_count, kind, host` — the same column set as `format.mjs`'s
714
+ `FIELDS`) and the lesson as the body:
707
715
 
708
716
  ```
709
717
  ~/.lorekit/ <repo>/.lorekit/ (opt-in)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.63.2",
3
+ "version": "1.64.1",
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": {
@@ -56,6 +56,10 @@ Findings are structural, not semantic — each names its rule:
56
56
  frozen at 1. Re-key it onto the structural pattern and move the identifier
57
57
  into the body.
58
58
  - **malformed-scope** — the scope string is invalid.
59
+ - **unkinded-state-record** — the value parses as a JSON object/array but the
60
+ entry has no `kind`, so it renders as a raw JSON blob in every SessionStart
61
+ digest instead of being excluded by `isGeneralLesson`. Set `--kind bus` or
62
+ `--kind signal` on the record.
59
63
 
60
64
  These are the cheapest wins and the least controversial, so clear them first.
61
65
  For each: either **fix it in place** (rewrite a too-short value into a real
@@ -311,6 +311,30 @@ export const LINT_RULES = {
311
311
  const reason = scopeIssue(e.scope);
312
312
  return reason ? `malformed scope: ${reason}` : null;
313
313
  },
314
+ // A CI state record (ci-state-records.md) is JSON on an entry left with no
315
+ // `kind`. `fetchLessons`'s `isGeneralLesson(kind)` filter — `kind == null ||
316
+ // kind === 'lesson'` — is a hard exclusion by kind, not by shape, so a record
317
+ // like this is NOT excluded from the SessionStart digest: it renders inline as
318
+ // a raw JSON blob, unreadable next to prose lessons. Conservative by design,
319
+ // like `volatile-key`: only a value that PARSES as a JSON object or array
320
+ // fires — a bare scalar (a plain lesson body that happens to be a number or a
321
+ // quoted string) is excluded. A short bare scalar is caught by `short-value`;
322
+ // a bare scalar at or above `MIN_VALUE_LEN` (e.g. a 20-char quoted string) is
323
+ // judged by no rule at all — a known gap, not a claim that `short-value`
324
+ // covers every bare scalar — and this rule has no opinion on it either way.
325
+ 'unkinded-state-record': (e) => {
326
+ if (e.kind != null) return null; // an explicit kind is exactly what this rule exists to require.
327
+ const v = String(e.value ?? '').trim();
328
+ if (!v) return null; // an empty value is `empty-value`'s to report.
329
+ let parsed;
330
+ try {
331
+ parsed = JSON.parse(v);
332
+ } catch {
333
+ return null; // not JSON — an ordinary prose lesson, not this rule's concern.
334
+ }
335
+ if (parsed === null || typeof parsed !== 'object') return null; // a bare scalar — short-value's to catch when short, otherwise unjudged.
336
+ return 'value is a JSON object/array with no kind set — it renders as a raw JSON blob in every SessionStart digest; set --kind bus or --kind signal';
337
+ },
314
338
  };
315
339
 
316
340
  // Run every lint rule against one normalized entry, returning the findings it
@@ -34,6 +34,13 @@ export const FIELDS = [
34
34
  // columns above: a file written before this existed decodes it as absent,
35
35
  // which the read projection reports as 0 rather than inventing a count.
36
36
  'seen_count',
37
+ // Taxonomy — `kind` ('lesson'/'bus'/'signal') and `host` (which agent loop
38
+ // wrote it), mirroring the hosted `memories.kind`/`memories.host` columns.
39
+ // Appended like the columns above: a file written before these existed
40
+ // decodes them as absent, which `normalizeEntry` in lessons-view.mjs falls
41
+ // back to inferring from tags for, same as an absent remote value.
42
+ 'kind',
43
+ 'host',
37
44
  ];
38
45
 
39
46
  // Serialize an entry ({ ...columns, value }) into file text.
@@ -100,7 +100,7 @@ class LocalStore {
100
100
  // contract's error surfacing.
101
101
  async write({
102
102
  scope, key, value, tags, source_agent, trigger, created_at, ttl_days, clear_ttl,
103
- origin_repo, origin_branch, origin_commit, origin_pr,
103
+ origin_repo, origin_branch, origin_commit, origin_pr, kind, host,
104
104
  } = {}) {
105
105
  const now = new Date().toISOString();
106
106
  const existing = this._findByKey(scope, key);
@@ -129,6 +129,10 @@ class LocalStore {
129
129
  origin_branch: origin_branch ?? existing?.entry.origin_branch ?? null,
130
130
  origin_commit: origin_commit ?? existing?.entry.origin_commit ?? null,
131
131
  origin_pr: origin_pr ?? existing?.entry.origin_pr ?? null,
132
+ // Taxonomy, same last-known-wins provenance as origin above: an
133
+ // omitted --kind/--host on a re-write must not erase a prior value.
134
+ kind: kind ?? existing?.entry.kind ?? null,
135
+ host: host ?? existing?.entry.host ?? null,
132
136
  created,
133
137
  updated: existing ? now : override || now,
134
138
  archived_at: null,
@@ -175,6 +179,8 @@ class LocalStore {
175
179
  origin_branch: entry.origin_branch ?? null,
176
180
  origin_commit: entry.origin_commit ?? null,
177
181
  origin_pr: entry.origin_pr ?? null,
182
+ kind: entry.kind ?? null,
183
+ host: entry.host ?? null,
178
184
  created: entry.created ?? now,
179
185
  updated: entry.updated ?? now,
180
186
  archived_at: entry.archived_at ?? null,
@@ -521,7 +521,7 @@ export const MCP_TOOL_DEFS = [
521
521
  "type": "integer",
522
522
  "minimum": 1,
523
523
  "maximum": 3650,
524
- "description": "Match lessons unseen for at least this many days. A never-seen lesson always matches."
524
+ "description": "Match lessons not individually opened via MCP or the CLI for at least this many days (a bulk list/search result or a dashboard view does not count). A never-opened lesson always matches."
525
525
  },
526
526
  "max_seen_count": {
527
527
  "type": "integer",
@@ -700,7 +700,7 @@ export const MCP_TOOL_DEFS = [
700
700
  "type": "integer",
701
701
  "minimum": 1,
702
702
  "maximum": 3650,
703
- "description": "Match lessons unseen for at least this many days. Omit to leave unchanged; pass explicit null to clear."
703
+ "description": "Match lessons not individually opened via MCP or the CLI for at least this many days (a bulk list/search result or a dashboard view does not count). Omit to leave unchanged; pass explicit null to clear."
704
704
  },
705
705
  "max_seen_count": {
706
706
  "type": "integer",
@@ -880,7 +880,7 @@ export const MCP_TOOL_DEFS = [
880
880
  "type": "integer",
881
881
  "minimum": 1,
882
882
  "maximum": 3650,
883
- "description": "Match lessons unseen for at least this many days. A never-seen lesson always matches."
883
+ "description": "Match lessons not individually opened via MCP or the CLI for at least this many days (a bulk list/search result or a dashboard view does not count). A never-opened lesson always matches."
884
884
  },
885
885
  "max_seen_count": {
886
886
  "type": "integer",
@@ -1044,7 +1044,7 @@ export const MCP_TOOL_DEFS = [
1044
1044
  "type": "integer",
1045
1045
  "minimum": 1,
1046
1046
  "maximum": 3650,
1047
- "description": "Match lessons unseen for at least this many days. A never-seen lesson always matches."
1047
+ "description": "Match lessons not individually opened via MCP or the CLI for at least this many days (a bulk list/search result or a dashboard view does not count). A never-opened lesson always matches."
1048
1048
  },
1049
1049
  "max_seen_count": {
1050
1050
  "type": "integer",