@lorekit/cli 1.31.0 → 1.32.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
@@ -600,7 +600,7 @@ const KNOWN_FLAGS = [
600
600
  'dir', 'project', 'global', 'endpoint', 'token', 'mode', 'store',
601
601
  'from', 'to', 'apply', 'yes', 'hooks', 'no-hooks', 'force', 'deep', 'adapter',
602
602
  'event', 'json', 'scope', 'threshold', 'help', 'version', 'telemetry',
603
- 'value', 'tags', 'source-agent', 'trigger', 'ttl-days', 'clear-ttl', 'org', 'remote', 'local',
603
+ 'value', 'tags', 'source-agent', 'trigger', 'kind', 'host', 'ttl-days', 'clear-ttl', 'org', 'remote', 'local',
604
604
  'link', 'base', 'q', 'owner', 'range', 'view', 'archived',
605
605
  'origin-repo', 'origin-branch', 'origin-commit', 'origin-pr', 'no-origin',
606
606
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.31.0",
3
+ "version": "1.32.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": {
@@ -37,16 +37,39 @@ export function scopeList({ projectScope, branchScope, repoScope } = {}) {
37
37
  return [...new Set([projectScope, branchScope, repoScope, 'global'].filter(Boolean))];
38
38
  }
39
39
 
40
+ // Infer { kind, host } from a memory's loop tags — the CLI-local mirror of
41
+ // `@lorekit/schemas` `inferKindHost` (the CLI has no schemas dependency). Kept
42
+ // deliberately small and in lockstep with that source, including the 64-char
43
+ // host clamp. Lets the offline store, whose rows carry no kind/host column,
44
+ // still be filtered and badged by taxonomy from the tags it does store.
45
+ function inferKindHostFromTags(tags) {
46
+ if (!Array.isArray(tags)) return {};
47
+ for (const tag of tags) {
48
+ if (tag === 'loop::review-outcomes') return { kind: 'bus', host: 'review' };
49
+ if (tag === 'loop::reviewer-comment-relevance') return { kind: 'signal', host: 'reviewer' };
50
+ const m = typeof tag === 'string' ? /^loop::(.+)-lessons$/.exec(tag) : null;
51
+ if (m && m[1] && m[1].length <= 64) return { kind: 'lesson', host: m[1] };
52
+ }
53
+ return {};
54
+ }
55
+
40
56
  // Normalize an entry from either store (local markdown row or hosted DB row)
41
57
  // into one stable shape the view + `--json` output can rely on. Remote rows may
42
- // spell the timestamp `updated_at`; local rows use `updated`.
58
+ // spell the timestamp `updated_at`; local rows use `updated`. When a row carries
59
+ // no explicit kind/host (every offline row, and any remote row written before
60
+ // migration 00056), fall back to the taxonomy inferred from its tags so the
61
+ // badge and the `--kind`/`--host` filter behave the same in both sections.
43
62
  export function normalizeEntry(e = {}) {
63
+ const tags = Array.isArray(e.tags) ? e.tags : [];
64
+ const inferred = inferKindHostFromTags(tags);
44
65
  return {
45
66
  scope: e.scope ?? null,
46
67
  key: e.key ?? null,
47
68
  value: e.value == null ? '' : String(e.value),
48
69
  updated: e.updated ?? e.updated_at ?? null,
49
- tags: Array.isArray(e.tags) ? e.tags : [],
70
+ tags,
71
+ kind: e.kind ?? inferred.kind ?? null,
72
+ host: e.host ?? inferred.host ?? null,
50
73
  };
51
74
  }
52
75
 
@@ -407,13 +430,27 @@ export function clusterDuplicates(entries = [], threshold = 0.8) {
407
430
  // `store.list({scope})` contract. Returns ordered per-scope groups plus a total
408
431
  // — a per-scope read failure is captured on the group, never thrown, so one bad
409
432
  // scope can't abort the listing. `store` may be a local or remote store.
410
- export async function gather(store, scopes) {
433
+ export async function gather(store, scopes, filters = {}) {
434
+ // Parse the taxonomy filters into value sets. `filters` is passed to the store
435
+ // too (the remote narrows server-side); we ALSO post-filter the normalized
436
+ // entries here so the offline store — which ignores kind/host in its own
437
+ // `list()` — stays consistent with remote. Post-filtering the remote rows is
438
+ // idempotent (they were already narrowed) and matches on the same inferred
439
+ // taxonomy a row without explicit columns gets in normalizeEntry.
440
+ const wanted = (v) =>
441
+ v == null ? null : new Set(String(v).split(',').map((s) => s.trim()).filter(Boolean));
442
+ const kindSet = wanted(filters.kind);
443
+ const hostSet = wanted(filters.host);
444
+ const keep = (e) =>
445
+ (!kindSet || (e.kind != null && kindSet.has(e.kind))) &&
446
+ (!hostSet || (e.host != null && hostSet.has(e.host)));
447
+
411
448
  const groups = [];
412
449
  let total = 0;
413
450
  for (const scope of scopes) {
414
451
  let res;
415
452
  try {
416
- res = await store.list({ scope });
453
+ res = await store.list({ scope, ...filters });
417
454
  } catch (e) {
418
455
  res = { ok: false, networkError: (e && e.message) || 'error' };
419
456
  }
@@ -421,7 +458,7 @@ export async function gather(store, scopes) {
421
458
  groups.push({ scope, entries: [], error: describeError(res) });
422
459
  continue;
423
460
  }
424
- const entries = (res.entries || []).map(normalizeEntry);
461
+ const entries = (res.entries || []).map(normalizeEntry).filter(keep);
425
462
  total += entries.length;
426
463
  groups.push({ scope, entries, error: null });
427
464
  }
@@ -456,7 +493,11 @@ export function renderSection(header, section) {
456
493
  }
457
494
  for (const e of g.entries) {
458
495
  const when = e.updated ? ` ${c.dim(`(updated ${shortDate(e.updated)})`)}` : '';
459
- log(` ${c.cyan('•')} ${g.scope}::${e.key}${when}`);
496
+ // Taxonomy badge — the kind (and host, when known) so the three families
497
+ // are visible at a glance in the list. Omitted for rows written before the
498
+ // taxonomy existed (NULL kind).
499
+ const badge = e.kind ? ` ${c.dim(`[${e.kind}${e.host ? `·${e.host}` : ''}]`)}` : '';
500
+ log(` ${c.cyan('•')} ${g.scope}::${e.key}${badge}${when}`);
460
501
  if (e.value) log(` ${c.dim(preview(e.value))}`);
461
502
  }
462
503
  }
package/src/list.mjs CHANGED
@@ -41,6 +41,11 @@ export async function list(args) {
41
41
  // Default to every applicable scope; `--scope <s>` narrows to one (an explicit
42
42
  // scope outside the applicable set is honoured — the user asked for it).
43
43
  const scopes = args.scope && typeof args.scope === 'string' ? [args.scope] : scopeList(scopeInfo);
44
+ // Optional taxonomy filters — `--kind lesson --host reviewer` narrows to one
45
+ // family/owner. Comma lists are honoured by the remote store's query builder.
46
+ const filters = {};
47
+ if (typeof args.kind === 'string') filters.kind = args.kind;
48
+ if (typeof args.host === 'string') filters.host = args.host;
44
49
 
45
50
  // `--link` short-circuits: print the Explorer deep link for the current
46
51
  // context (the most-specific applicable scope, or `--scope`), no store reads.
@@ -64,9 +69,9 @@ export async function list(args) {
64
69
  // agent-facing control model enforces, honored here in the human read view.
65
70
  const { localDenied, remoteDenied } = resolveDenies(root, { env });
66
71
 
67
- const offline = localDenied ? { groups: [], total: 0 } : await gather(local, scopes);
72
+ const offline = localDenied ? { groups: [], total: 0 } : await gather(local, scopes, filters);
68
73
  const remoteAvailable = !remoteDenied && remote.usable();
69
- const remoteResult = remoteAvailable ? await gather(remote, scopes) : { groups: [], total: 0 };
74
+ const remoteResult = remoteAvailable ? await gather(remote, scopes, filters) : { groups: [], total: 0 };
70
75
 
71
76
  const offlineSection = localDenied
72
77
  ? { available: false, reason: `disabled by deny constraint (${localDenied.source})` }
@@ -58,10 +58,12 @@ class RemoteStore {
58
58
 
59
59
  // ── Memory operations → REST ──────────────────────────────────────────────
60
60
 
61
- async list({ scope, tags, limit } = {}) {
61
+ async list({ scope, tags, kind, host, limit } = {}) {
62
62
  const p = new URLSearchParams();
63
63
  if (scope) p.set('scope', scope);
64
64
  if (tags?.length) p.set('tags', Array.isArray(tags) ? tags.join(',') : tags);
65
+ if (kind) p.set('kind', Array.isArray(kind) ? kind.join(',') : kind);
66
+ if (host) p.set('host', Array.isArray(host) ? host.join(',') : host);
65
67
  if (limit) p.set('limit', String(limit));
66
68
  const res = await this._rest(`/memories?${p}`);
67
69
  if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
@@ -92,13 +94,15 @@ class RemoteStore {
92
94
 
93
95
  async write(args = {}) {
94
96
  const {
95
- scope, key, value, tags, source_agent, trigger, org, ttl_days, clear_ttl, created_at,
97
+ scope, key, value, tags, source_agent, trigger, kind, host, org, ttl_days, clear_ttl, created_at,
96
98
  origin_repo, origin_branch, origin_commit, origin_pr,
97
99
  } = args;
98
100
  const body = { scope, key, value };
99
101
  if (tags !== undefined) body.tags = tags;
100
102
  if (source_agent !== undefined) body.source_agent = source_agent;
101
103
  if (trigger !== undefined) body.trigger = trigger;
104
+ if (kind !== undefined) body.kind = kind;
105
+ if (host !== undefined) body.host = host;
102
106
  if (org !== undefined) body.org = org;
103
107
  if (ttl_days !== undefined) body.ttl_days = ttl_days;
104
108
  if (clear_ttl !== undefined) body.clear_ttl = clear_ttl;
package/src/write.mjs CHANGED
@@ -120,6 +120,10 @@ export async function write(args) {
120
120
  const tags = args.tags ? String(args.tags).split(',').map((t) => t.trim()).filter(Boolean) : [];
121
121
  const sourceAgent = typeof args['source-agent'] === 'string' ? args['source-agent'] : undefined;
122
122
  const trigger = typeof args.trigger === 'string' ? args.trigger : undefined;
123
+ // Taxonomy overrides. Omitted → the server infers kind/host from a
124
+ // `loop::<host>-lessons` tag, so a tagged write needs neither flag.
125
+ const kind = typeof args.kind === 'string' ? args.kind : undefined;
126
+ const host = typeof args.host === 'string' ? args.host : undefined;
123
127
  // `--ttl-days` is validated HERE, at the flag seam, rather than being left to the
124
128
  // store: a truthiness test silently swallowed `--ttl-days 0` (falsy) and
125
129
  // `--ttl-days abc` (NaN, dropped again by the `ttl_days` spread further down), so
@@ -258,6 +262,8 @@ export async function write(args) {
258
262
  ...(tags.length ? { tags } : {}),
259
263
  ...(sourceAgent ? { source_agent: sourceAgent } : {}),
260
264
  ...(trigger ? { trigger } : {}),
265
+ ...(kind ? { kind } : {}),
266
+ ...(host ? { host } : {}),
261
267
  ...(ttlDays ? { ttl_days: ttlDays } : {}),
262
268
  ...(clearTtl ? { clear_ttl: true } : {}),
263
269
  ...(orgSlug ? { org: orgSlug } : {}),