@lorekit/cli 1.27.0 → 1.28.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 CHANGED
@@ -321,6 +321,7 @@ lorekit link global # the Explorer filtered to global scop
321
321
  lorekit link repo::owner/repo prefer-guards # open one lesson's detail sheet
322
322
  lorekit link global::prefer-guards --json # { url, surface, base, params }
323
323
  lorekit url --q "flaky test" --owner personal # search + ownership filter
324
+ lorekit link global --tags "perf,ci" # Explorer filtered to labels
324
325
  ```
325
326
 
326
327
  With no arguments it links to the cwd's **most-specific scope** ("share what I'm
@@ -331,8 +332,9 @@ that lesson's detail sheet. It sets **both** the `lesson` param (which opens the
331
332
  sheet) and `scope` — not because scope is needed to find the lesson (the sidebar
332
333
  reads one unfiltered recent set), but so the Explorer list *behind* the sheet is
333
334
  filtered to the lesson's own scope. Filter flags mirror the Explorer: `--q`
334
- (search), `--owner <all|personal|orgId>`, `--range`/`--from`/`--to`, `--archived`,
335
- `--view <scope|time>`.
335
+ (search), `--owner <all|personal|orgId>`, `--tags <a,b,c>` (label filter, AND
336
+ across labels; comma-separated or a JSON array), `--range`/`--from`/`--to`,
337
+ `--archived`, `--view <scope|time>`.
336
338
 
337
339
  Every param is `encodeURIComponent(JSON.stringify(value))` — the exact inverse of
338
340
  how the dashboard's `useUrlState` reads it back (`JSON.parse`, falling back to the
package/bin/lorekit.mjs CHANGED
@@ -85,8 +85,8 @@ ${c.bold('Commands')}
85
85
  link (url) Print a shareable dashboard deep-link URL for the current context,
86
86
  a scope, or a specific lesson (opens its detail sheet). No args
87
87
  links to the cwd's most-specific scope. Filter flags mirror the
88
- Explorer (--q / --owner / --range / --archived / --view); --base or
89
- LOREKIT_APP_URL override the dashboard host. --json. Pipe it:
88
+ Explorer (--q / --owner / --tags / --range / --archived / --view);
89
+ --base or LOREKIT_APP_URL override the dashboard host. --json. Pipe it:
90
90
  lorekit link | pbcopy.
91
91
  bootstrap Apply the BYOD schema to a user-supplied Supabase database.
92
92
  Only needed when using LOREKIT_STORAGE_URL / LOREKIT_STORAGE_ANON_KEY.
@@ -506,6 +506,7 @@ ${c.bold('Options')}
506
506
  --scope <scope> Scope to link to (when no positional scope is given)
507
507
  --q <text> Pre-fill the Explorer search box
508
508
  --owner <o> Ownership filter: all | personal | <orgId>
509
+ --tags <a,b,c> Label filter (AND across labels); comma-separated or a JSON array
509
510
  --range <json> Date range as {"from":"YYYY-MM-DD","to":"YYYY-MM-DD"}
510
511
  --from <date> Range start (shorthand for --range)
511
512
  --to <date> Range end (shorthand for --range)
@@ -521,6 +522,7 @@ ${c.bold('Examples')}
521
522
  npx @lorekit/cli link repo::owner/repo prefer-guards # open one lesson's detail sheet
522
523
  npx @lorekit/cli link global::prefer-guards --json # { url, surface, base, params }
523
524
  npx @lorekit/cli url --q "flaky test" --owner personal # search + ownership filter
525
+ npx @lorekit/cli link global --tags "perf,ci" # Explorer filtered to labels
524
526
  `,
525
527
  migrate: `${c.bold('lorekit migrate')} — relocate a LoreKit-format local store into the current layout
526
528
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.27.0",
3
+ "version": "1.28.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": {
@@ -29,14 +29,17 @@ export const LORE_PARAM_DEFAULTS = {
29
29
  q: '', // string search query
30
30
  range: null, // { from, to } | null (DateRange, "YYYY-MM-DD")
31
31
  owner: 'all', // 'all' | 'personal' | { orgId }
32
+ tags: [], // string[] — label filter (AND across labels); [] means "no filter"
32
33
  view: 'scope', // 'scope' | 'time'
33
34
  archived: false, // boolean
34
35
  lesson: null, // { scope, key } | null — opens the detail sheet
35
36
  };
36
37
 
37
38
  // A stable, readable param order (also makes URLs deterministic for tests).
38
- // `scope` precedes `lesson` so a lesson link reads `?scope=…&lesson=…`.
39
- const PARAM_ORDER = ['scope', 'q', 'range', 'owner', 'view', 'archived', 'lesson'];
39
+ // Mirrors the `useUrlState` call order in `LoreExplorer.tsx` (+ the `lesson`
40
+ // param last), so `tags` sits between `owner` and `view`. `scope` precedes
41
+ // `lesson` so a lesson link reads `?scope=…&lesson=…`.
42
+ const PARAM_ORDER = ['scope', 'q', 'range', 'owner', 'tags', 'view', 'archived', 'lesson'];
40
43
 
41
44
  // Strip trailing slashes from a base URL, falling back to the default when the
42
45
  // input is empty/absent. Pure.
@@ -154,6 +157,44 @@ export function parseViewArg(view) {
154
157
  return view === 'time' ? 'time' : 'scope';
155
158
  }
156
159
 
160
+ // Coerce the `--tags` flag to a normalized `string[]` label filter, mirroring the
161
+ // web app's `normalizeTags` (`packages/web/src/lib/tag-filter.ts`): trim each
162
+ // entry, drop empties, de-duplicate, preserve first-seen order. Accepts either a
163
+ // JSON array string (`'["perf","ci"]'`) or the friendlier comma-separated form
164
+ // (`'perf, ci'`); a malformed JSON array falls back to comma-splitting rather
165
+ // than throwing. Returns `[]` for absent/empty input (the default → omitted from
166
+ // the URL). Pure.
167
+ export function parseTagsArg(tags) {
168
+ if (Array.isArray(tags)) return normalizeTagList(tags);
169
+ if (typeof tags !== 'string' || !tags.trim()) return [];
170
+ const s = tags.trim();
171
+ if (s.startsWith('[')) {
172
+ try {
173
+ const parsed = JSON.parse(s);
174
+ if (Array.isArray(parsed)) return normalizeTagList(parsed);
175
+ } catch {
176
+ /* malformed JSON array → fall through to comma-splitting */
177
+ }
178
+ }
179
+ return normalizeTagList(s.split(','));
180
+ }
181
+
182
+ // Trim, drop non-string/empty entries, and de-duplicate preserving order. The
183
+ // CLI-side twin of the web's `normalizeTags`; kept inline to keep this module
184
+ // zero-import. Pure.
185
+ function normalizeTagList(list) {
186
+ const seen = new Set();
187
+ const out = [];
188
+ for (const item of list) {
189
+ if (typeof item !== 'string') continue;
190
+ const t = item.trim();
191
+ if (!t || seen.has(t)) continue;
192
+ seen.add(t);
193
+ out.push(t);
194
+ }
195
+ return out;
196
+ }
197
+
157
198
  // Coerce the date-range flags to a `{ from, to }` DateRange or null. `--range`
158
199
  // (a JSON object string) wins; else `--from`/`--to` shorthand builds one (both
159
200
  // keys always present so the shape matches the app's DateRange). A malformed
package/src/link.mjs CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  parseOwnerArg,
26
26
  parseViewArg,
27
27
  parseRangeArg,
28
+ parseTagsArg,
28
29
  resolveScopeArg,
29
30
  surfaceFor,
30
31
  } from './deeplink-pure.mjs';
@@ -66,6 +67,7 @@ export async function link(args) {
66
67
  const owner = parseOwnerArg(args.owner);
67
68
  const view = parseViewArg(args.view);
68
69
  const range = parseRangeArg(args);
70
+ const tags = parseTagsArg(args.tags);
69
71
  const archived = Boolean(args.archived);
70
72
 
71
73
  const gaveAnyInput =
@@ -75,6 +77,7 @@ export async function link(args) {
75
77
  owner !== 'all' ||
76
78
  view !== 'scope' ||
77
79
  range !== null ||
80
+ tags.length > 0 ||
78
81
  archived;
79
82
 
80
83
  // Bare `lorekit link` (no scope, no lesson, no filters) → the cwd's
@@ -90,6 +93,7 @@ export async function link(args) {
90
93
  if (key) params.lesson = { scope, key };
91
94
  if (q) params.q = q;
92
95
  if (owner !== 'all') params.owner = owner;
96
+ if (tags.length) params.tags = tags;
93
97
  if (view !== 'scope') params.view = view;
94
98
  if (range !== null) params.range = range;
95
99
  if (archived) params.archived = true;