Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,20 @@ without moving its version. See

Alias `ls` — read-only human view of the lessons for the cwd's scopes, project/branch/repo/global,
split into an **Offline** local-store section and a **Remote** hosted section that degrades
gracefully when unconfigured; `--json`/`--scope`; `--all` drains all pages via `gatherStream`
gracefully when unconfigured; `--json`/`--scope`; the eight dimension filters `policy`/`groom`
take — `--tags a,b` combined by `--tags-mode any|all|none` [default `any`, the hosted
`GET /memories` default], and `--kind`/`--host`/`--trigger`/`--source-agent`/`--origin-repo`/
`--origin-branch`/`--origin-pr a,b`, each with `--<dim>-mode in|nin` [default `in`]; `nin` follows
the hosted `lorekit_match_text`/`lorekit_match_int` rule (migration 00066): the value must be SET
and none of the list, so a row without that field matches neither mode; `--origin-pr` takes PR
numbers only (`#482` accepted, `007` reads as 7), because the hosted route DROPS a non-numeric
value and then applies no PR filter. All are parsed by `parseReadFilters` (`src/shared/flags.mjs`,
reusing the `policy`/`groom` `DIMENSIONS` table), forwarded to the remote, AND applied to both
stores by the one `entryFilter` predicate in `lessons-view.mjs` over `normalizeEntry`'s output,
which carries `source_agent`/`trigger`/`origin_*` for that reason (so `--json` shows them too; the
pure `matchesTags` lives in `lessons-pure.mjs` because the local store applies it too); a bare
value flag, or a mode without its value flag, is a usage error rather than an unfiltered listing;
`--all` drains all pages via `gatherStream`
[default is single-page `gather()`]; `--max <n>` hard-caps entries surveyed [default 5000];
`--since`/`--until <iso-date>` forward `created_since`/`created_until` to the REST request;
independently builds BOTH stores via `src/shared/stores.mjs` + renders via the reusable
Expand All @@ -111,7 +124,9 @@ case-insensitive** substring against a lesson's key OR value, via the pure `matc
dependency-free `lessons-pure.mjs`, re-exported by `lessons-view.mjs`)/`filterGroups` helpers over
the shared `gather()` output — NOT each store's native `search` method, whose semantics diverge
(local literal-substring vs remote server-FTS); regex metachars are matched verbatim, never
compiled; `--json`/`--scope`
compiled; `--json`/`--scope`; the same eight dimension filters as `list` (`--tags` +
`--tags-mode any|all|none`; `--kind`/`--host`/`--trigger`/`--source-agent`/`--origin-*` +
`--<dim>-mode in|nin`) narrow the candidates before the query is matched

### `show`

Expand Down
2 changes: 2 additions & 0 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ List soft-archived lessons for a scope, newest archived first. Requires a token
"name": "memory.list_archived",
"arguments": {
"scope": "global",
"tags": ["loop::aw-lessons"],
"limit": 20
}
}
Expand All @@ -545,6 +546,7 @@ List soft-archived lessons for a scope, newest archived first. Requires a token
| Argument | Default | Description |
|----------|---------|-------------|
| `scope` | all visible scopes | Scope to list archived entries for. Omit to list across every scope you can see |
| `tags` | | Only entries carrying ANY of these tags (OR) — the same semantics as `memory.list` |
| `limit` | `50` | Max results (cap: 100) |

**Returns:** `{ "entries": [{ "scope", "key", "value", "tags", "updated_at", "archived_at" }] }`
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/bin/lorekit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,13 @@ short note on how to set it up — never an error.
${c.bold('Options')}
-d, --dir <path> Target project root (default: current directory)
--scope <scope> Restrict to a single scope (default: all applicable)
--tags <a,b> Only memories carrying these tags (combined by --tags-mode)
--tags-mode <m> any (at least one, default) | all (every one) | none
--kind, --host, --trigger, --source-agent,
--origin-repo, --origin-branch, --origin-pr <a,b>
Only memories whose field is one of these values
(kind: lesson|bus|signal; origin-pr: PR numbers)
--<dim>-mode <m> in (is one of them, default) | nin (is set, and none of them)
--json Machine-readable output
-e, --endpoint <url> Remote endpoint override (else .mcp.json / LOREKIT_MCP_URL)
-t, --token <token> Remote token override (else .mcp.json / LOREKIT_TOKEN)
Expand All @@ -360,6 +367,12 @@ ${c.bold('Examples')}
npx @lorekit/cli list
npx @lorekit/cli list --json
npx @lorekit/cli list --scope global
npx @lorekit/cli list --tags loop::aw-lessons
npx @lorekit/cli list --tags perf,ci --tags-mode all
npx @lorekit/cli list --kind lesson --host reviewer
npx @lorekit/cli list --kind bus --kind-mode nin
npx @lorekit/cli list --source-agent claude --origin-pr 482
npx @lorekit/cli list --origin-branch main --origin-branch-mode nin
npx @lorekit/cli list --scope global --link
`,
search: `${c.bold('lorekit search')} — full-text search the applicable memories ${c.dim('(alias: grep)')}
Expand All @@ -376,6 +389,13 @@ never interpreted). Results are shown in the same Offline / Remote split as
${c.bold('Options')}
-d, --dir <path> Target project root (default: current directory)
--scope <scope> Restrict to a single scope (default: all applicable)
--tags <a,b> Only search memories carrying these tags (combined by --tags-mode)
--tags-mode <m> any (at least one, default) | all (every one) | none
--kind, --host, --trigger, --source-agent,
--origin-repo, --origin-branch, --origin-pr <a,b>
Only search memories whose field is one of these values
(kind: lesson|bus|signal; origin-pr: PR numbers)
--<dim>-mode <m> in (is one of them, default) | nin (is set, and none of them)
--json Machine-readable output
-e, --endpoint <url> Remote endpoint override (else .mcp.json / LOREKIT_MCP_URL)
-t, --token <token> Remote token override (else .mcp.json / LOREKIT_TOKEN)
Expand All @@ -386,6 +406,9 @@ ${c.bold('Examples')}
npx @lorekit/cli search sandbox
npx @lorekit/cli grep "flaky test" --json
npx @lorekit/cli search migration --scope global
npx @lorekit/cli search migration --tags loop::aw-lessons
npx @lorekit/cli search flaky --kind lesson --host-mode nin --host ci
npx @lorekit/cli search flaky --trigger stuck-loop --origin-repo mthines/lorekit
npx @lorekit/cli search "flaky test" --scope global --link
`,
write: `${c.bold('lorekit write')} — create or update a memory from the CLI
Expand Down
37 changes: 20 additions & 17 deletions packages/cli/src/commands/list.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import { resolveProjectRoot } from '../shared/config.mjs';
import { deriveScope } from '../shared/scope.mjs';
import { resolveDenies } from '../shared/control.mjs';
import { resolveStores, remoteUnavailableReason } from '../shared/stores.mjs';
import { scopeList, gather, gatherStream, renderSection, DEFAULT_MAX } from '../shared/lessons-view.mjs';
import { scopeList, gather, gatherStream, renderSection, entryFilter, DEFAULT_MAX } from '../shared/lessons-view.mjs';
import { resolveAppBase, mostSpecificScope } from '../shared/deeplink-pure.mjs';
import { parseReadFilters } from '../shared/flags.mjs';
import { emitLink } from './link.mjs';
import { log, heading, c } from '../shared/util.mjs';
import { log, err, heading, c } from '../shared/util.mjs';

// Abbreviate the user's home directory to `~` for readable paths.
function prettyPath(p) {
Expand All @@ -41,11 +42,16 @@ export async function list(args) {
// Default to every applicable scope; `--scope <s>` narrows to one (an explicit
// scope outside the applicable set is honoured — the user asked for it).
const scopes = args.scope && typeof args.scope === 'string' ? [args.scope] : scopeList(scopeInfo);
// Optional taxonomy filters — `--kind lesson --host reviewer` narrows to one
// family/owner. Comma lists are honoured by the remote store's query builder.
const filters = {};
if (typeof args.kind === 'string') filters.kind = args.kind;
if (typeof args.host === 'string') filters.host = args.host;
// Optional dimension filters, ANDed — the eight `policy`/`groom` take:
// `--tags a,b [--tags-mode any|all|none]` (`memory.list`'s semantics), and
// `--kind`/`--host`/`--trigger`/`--source-agent`/`--origin-repo`/
// `--origin-branch`/`--origin-pr a,b`, each negated by `--<dim>-mode nin`.
const parsed = parseReadFilters(args);
if (parsed.error) {
err(`${c.red('Error:')} ${parsed.error}`);
return 1;
}
const filters = parsed.filters;

// `--link` short-circuits: print the Explorer deep link for the current
// context (the most-specific applicable scope, or `--scope`), no store reads.
Expand Down Expand Up @@ -77,16 +83,10 @@ export async function list(args) {
const surveySince = args.since || undefined;
const surveyUntil = args.until || undefined;

// Taxonomy `keep` predicate mirroring `gather()`'s: `gatherStream` neither
// forwards `kind`/`host` to the store nor post-filters, so a `--all` drain
// must apply the same narrowing here or `list --all --kind X` would ignore X.
const wanted = (v) =>
v == null ? null : new Set(String(v).split(',').map((s) => s.trim()).filter(Boolean));
const kindSet = wanted(filters.kind);
const hostSet = wanted(filters.host);
const keep = (e) =>
(!kindSet || (e.kind != null && kindSet.has(e.kind))) &&
(!hostSet || (e.host != null && hostSet.has(e.host)));
// The SAME `keep` predicate `gather()` applies: `gatherStream` neither
// forwards the filters to the store nor post-filters, so a `--all` drain must
// apply the narrowing here or `list --all --tags X` would ignore X.
const keep = entryFilter(filters);

const offline = localDenied ? { groups: [], total: 0 } : await gather(local, scopes, filters);
const remoteAvailable = !remoteDenied && remote.usable();
Expand Down Expand Up @@ -157,6 +157,9 @@ export async function list(args) {
'lorekit.cli.list.offline_count': offline.total,
'lorekit.cli.list.remote_count': remoteResult.total,
'lorekit.cli.list.remote_available': remoteAvailable,
// How many of the eight dimension filters were applied (0–8) — a count,
// never a value.
'lorekit.cli.list.filter_count': Object.keys(filters).filter((k) => !k.endsWith('_mode')).length,
};
}

Expand Down
21 changes: 18 additions & 3 deletions packages/cli/src/commands/search.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { resolveDenies } from '../shared/control.mjs';
import { resolveStores, remoteUnavailableReason } from '../shared/stores.mjs';
import { scopeList, gather, filterGroups, renderSection } from '../shared/lessons-view.mjs';
import { resolveAppBase, mostSpecificScope } from '../shared/deeplink-pure.mjs';
import { parseReadFilters } from '../shared/flags.mjs';
import { emitLink } from './link.mjs';
import { log, err, heading, c } from '../shared/util.mjs';

Expand All @@ -33,7 +34,7 @@ export async function search(args) {
// and would otherwise degrade to `list`.
const query = typeof args._[1] === 'string' ? args._[1] : '';
if (!query.trim()) {
err(`${c.red('Usage:')} lorekit search <query> [--scope <s>] [--json]`);
err(`${c.red('Usage:')} lorekit search <query> [--scope <s>] [--tags <a,b> [--tags-mode any|all|none]] [--kind|--host|--trigger|--source-agent|--origin-repo|--origin-branch|--origin-pr <a,b> [--<dim>-mode in|nin]] [--json]`);
err(`Provide a search term. Run ${c.cyan('lorekit search --help')} for options.`);
return 1;
}
Expand All @@ -43,6 +44,17 @@ export async function search(args) {
// scope outside the applicable set is honoured — the user asked for it).
const scopes = args.scope && typeof args.scope === 'string' ? [args.scope] : scopeList(scopeInfo);

// The eight dimension filters (`--tags` + `--tags-mode any|all|none`, and
// `--kind`/`--host`/`--trigger`/`--source-agent`/`--origin-*` + `--<dim>-mode
// in|nin`) narrow the candidate set before the query is matched — the same
// filters, with the same semantics, as `list`.
const parsed = parseReadFilters(args);
if (parsed.error) {
err(`${c.red('Error:')} ${parsed.error}`);
return 1;
}
const filters = parsed.filters;

// `--link` short-circuits: print the Explorer deep link for this search
// (`?q=…` plus the most-specific applicable scope), without touching a store.
if (args.link) {
Expand All @@ -65,10 +77,10 @@ export async function search(args) {

const offline = localDenied
? { groups: [], total: 0 }
: filterGroups(await gather(local, scopes), query);
: filterGroups(await gather(local, scopes, filters), query);
const remoteAvailable = !remoteDenied && remote.usable();
const remoteResult = remoteAvailable
? filterGroups(await gather(remote, scopes), query)
? filterGroups(await gather(remote, scopes, filters), query)
: { groups: [], total: 0 };

const offlineSection = localDenied
Expand Down Expand Up @@ -108,6 +120,9 @@ export async function search(args) {
'lorekit.cli.search.offline_matches': offline.total,
'lorekit.cli.search.remote_matches': remoteResult.total,
'lorekit.cli.search.remote_available': remoteAvailable,
// How many of the eight dimension filters were applied (0–8) — a count,
// never a value.
'lorekit.cli.search.filter_count': Object.keys(filters).filter((k) => !k.endsWith('_mode')).length,
};
}

Expand Down
28 changes: 22 additions & 6 deletions packages/cli/src/shared/completions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,26 @@ const FLAG = {
'cluster-by-key': { desc: 'Cluster by shared key capture', arg: 'regex' },
value: { desc: 'Memory value', arg: 'text' },
tags: { desc: 'Comma-separated tags', arg: 'a,b,c' },
'source-agent': { desc: 'Source agent name to record', arg: 'name' },
'tags-mode': { desc: 'How --tags combine', arg: 'mode', values: ['any', 'all', 'none'] },
kind: { desc: 'Bucket kind filter', arg: 'a,b', values: ['lesson', 'bus', 'signal'] },
'kind-mode': { desc: 'How --kind matches', arg: 'mode', values: ['in', 'nin'] },
host: { desc: 'Owning host filter', arg: 'a,b' },
'host-mode': { desc: 'How --host matches', arg: 'mode', values: ['in', 'nin'] },
'source-agent': { desc: 'Source agent name', arg: 'name' },
'source-agent-mode': { desc: 'How --source-agent matches', arg: 'mode', values: ['in', 'nin'] },
trigger: { desc: 'Trigger context slug', arg: 'slug' },
'trigger-mode': { desc: 'How --trigger matches', arg: 'mode', values: ['in', 'nin'] },
'ttl-days': { desc: 'Days until auto-expiry (1..365)', arg: 'n' },
'clear-ttl': { desc: 'Remove any existing expiry' },
org: { desc: "Write to this org's scope (remote)", arg: 'slug' },
cited: { desc: 'scope::key refs this lesson applied (remote)', arg: 'a,b' },
'origin-repo': { desc: 'Override the provenance repository', arg: 'owner/name' },
'origin-branch': { desc: 'Override the provenance branch', arg: 'branch' },
'origin-repo': { desc: 'Provenance repository', arg: 'owner/name' },
'origin-repo-mode': { desc: 'How --origin-repo matches', arg: 'mode', values: ['in', 'nin'] },
'origin-branch': { desc: 'Provenance branch', arg: 'branch' },
'origin-branch-mode': { desc: 'How --origin-branch matches', arg: 'mode', values: ['in', 'nin'] },
'origin-commit': { desc: 'Override the provenance commit', arg: 'sha' },
'origin-pr': { desc: 'The pull request this came out of', arg: 'n' },
'origin-pr': { desc: 'Pull request number(s)', arg: 'n' },
'origin-pr-mode': { desc: 'How --origin-pr matches', arg: 'mode', values: ['in', 'nin'] },
'no-origin': { desc: 'Record no provenance at all' },
remote: { desc: 'Force the remote store' },
local: { desc: 'Force the local offline store' },
Expand Down Expand Up @@ -111,10 +121,16 @@ const COMMANDS = [
{ name: 'update', summary: 'Offline refresh of the bundled skills to the shipped version',
flags: ['dir', 'project', 'global', 'check'] },
{ name: 'list', summary: 'List memories for the current directory', aliases: ['ls'],
flags: ['dir', 'scope', 'json', 'endpoint', 'token', 'store', 'link', 'base'] },
flags: ['dir', 'scope', 'tags', 'tags-mode', 'kind', 'kind-mode', 'host', 'host-mode',
'trigger', 'trigger-mode', 'source-agent', 'source-agent-mode', 'origin-repo', 'origin-repo-mode',
'origin-branch', 'origin-branch-mode', 'origin-pr', 'origin-pr-mode',
'json', 'endpoint', 'token', 'store', 'link', 'base'] },
{ name: 'search', summary: 'Full-text search the applicable memories', aliases: ['grep'],
positional: 'query',
flags: ['dir', 'scope', 'json', 'endpoint', 'token', 'store', 'link', 'base'] },
flags: ['dir', 'scope', 'tags', 'tags-mode', 'kind', 'kind-mode', 'host', 'host-mode',
'trigger', 'trigger-mode', 'source-agent', 'source-agent-mode', 'origin-repo', 'origin-repo-mode',
'origin-branch', 'origin-branch-mode', 'origin-pr', 'origin-pr-mode',
'json', 'endpoint', 'token', 'store', 'link', 'base'] },
{ name: 'show', summary: 'Inspect one memory in full', positional: 'address',
flags: ['dir', 'json', 'endpoint', 'token', 'store', 'scope', 'key', 'link', 'base'] },
{ name: 'stats', summary: 'Count memories per scope and store',
Expand Down
Loading
Loading