@lorekit/cli 1.64.1 → 1.65.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
@@ -103,7 +103,7 @@ ${c.bold('Commands')}
103
103
  purge: remote only, account-wide, irreversible, --yes required
104
104
  non-interactively. Takes no options.
105
105
  groom Preview (default) or --run a retention sweep: --policy-id <id> or
106
- --scope <s> [+ --min-age-days/--unseen-days/--max-seen-count].
106
+ --scope <s> [+ --min-age-days/--unseen-days/--max-seen-count/--max-read-count].
107
107
  Remote only. --run soft-archives matches (recoverable via
108
108
  restore); prompts for confirmation, --yes to skip. --json.
109
109
  policy Manage saved retention rules: list / create / update / delete.
@@ -899,7 +899,7 @@ ${c.bold('Options')}
899
899
 
900
900
  ${c.bold('Usage')}
901
901
  lorekit groom --policy-id <id> [--run] [--yes] [--json]
902
- lorekit groom --scope <s> [--min-age-days <n>] [--unseen-days <n>] [--max-seen-count <n>] [--run] [--yes] [--json]
902
+ lorekit groom --scope <s> [--min-age-days <n>] [--unseen-days <n>] [--max-seen-count <n>] [--max-read-count <n>] [--run] [--yes] [--json]
903
903
 
904
904
  Resolves the SAME candidates a saved policy or an inline condition set would
905
905
  catch, via the retention-policy candidate query — a previewed count always
@@ -918,7 +918,9 @@ ${c.bold('Options')}
918
918
  --scope <s> Inline scope to match (mutually exclusive with --policy-id)
919
919
  --min-age-days <n> Match only lessons at least n days old
920
920
  --unseen-days <n> Match lessons unseen for at least n days (never-seen always matches)
921
- --max-seen-count <n> Match only lessons that recurred at most n times
921
+ --max-seen-count <n> Match only lessons that recurred at most n times (WRITES)
922
+ --max-read-count <n> Match only lessons READ at most n times. Bulk list/search
923
+ reads count here, unlike --unseen-days
922
924
  --run Archive the matches instead of previewing
923
925
  -y, --yes Confirm --run; required when non-interactive
924
926
  --json Machine-readable result
@@ -932,12 +934,13 @@ ${c.bold('Usage')}
932
934
  lorekit policy list [--json]
933
935
  lorekit policy create --scope <s> --name <n> [--mode review|auto] [--enabled]
934
936
  [--min-age-days <n>] [--unseen-days <n>] [--max-seen-count <n>]
937
+ [--max-read-count <n>]
935
938
  lorekit policy update <id> [--name <n>] [--mode review|auto] [--enabled|--disabled]
936
939
  [--min-age-days <n>|--clear-min-age-days] [...] [--json]
937
940
  lorekit policy delete <id> [--yes] [--json]
938
941
 
939
942
  A policy is a saved retention rule: a scope plus AND-ed conditions
940
- (min-age-days / unseen-days / max-seen-count). \`mode: review\` surfaces it for
943
+ (min-age-days / unseen-days / max-seen-count / max-read-count). \`mode: review\` surfaces it for
941
944
  you to run by hand with ${c.cyan('lorekit groom --policy-id')}; \`mode: auto\` gets swept
942
945
  nightly, but ONLY once you also pass --enabled — auto starts disabled on
943
946
  every new policy so a saved rule never archives anything unattended.
@@ -949,9 +952,9 @@ ${c.bold('Options')}
949
952
  --name <n> Policy name (create) / new name (update)
950
953
  --mode <review|auto> Match mode (create/update)
951
954
  --enabled / --disabled Turn auto-mode on/off (create/update)
952
- --min-age-days <n>, --unseen-days <n>, --max-seen-count <n>
955
+ --min-age-days <n>, --unseen-days <n>, --max-seen-count <n>, --max-read-count <n>
953
956
  Conditions (create/update)
954
- --clear-min-age-days, --clear-unseen-days, --clear-max-seen-count
957
+ --clear-min-age-days, --clear-unseen-days, --clear-max-seen-count, --clear-max-read-count
955
958
  Remove a condition (update only)
956
959
  -y, --yes Confirm delete; required when non-interactive
957
960
  --json Machine-readable result
@@ -1071,9 +1074,9 @@ const KNOWN_FLAGS = [
1071
1074
  // `invariants candidates`
1072
1075
  'min-seen-count',
1073
1076
  // groom / policy / protect / pin / unpin
1074
- 'policy-id', 'min-age-days', 'unseen-days', 'max-seen-count', 'run',
1077
+ 'policy-id', 'min-age-days', 'unseen-days', 'max-seen-count', 'max-read-count', 'run',
1075
1078
  'name', 'mode', 'enabled', 'disabled',
1076
- 'clear-min-age-days', 'clear-unseen-days', 'clear-max-seen-count', 'off',
1079
+ 'clear-min-age-days', 'clear-unseen-days', 'clear-max-seen-count', 'clear-max-read-count', 'off',
1077
1080
  // `obligations`
1078
1081
  'files', 'strict', 'strict-all',
1079
1082
  ];
@@ -1088,7 +1091,7 @@ async function main() {
1088
1091
  const argv = process.argv.slice(2);
1089
1092
  const args = parseArgs(argv, {
1090
1093
  aliases: { d: 'dir', e: 'endpoint', t: 'token', y: 'yes', h: 'help', v: 'version' },
1091
- booleans: ['yes', 'force', 'deep', 'apply', 'help', 'version', 'global', 'project', 'no-hooks', 'mcp-json', 'no-origin', 'json', 'remote', 'local', 'link', 'archived', 'clear-ttl', 'telemetry', 'all', 'run', 'enabled', 'disabled', 'off', 'clear-min-age-days', 'clear-unseen-days', 'clear-max-seen-count', 'strict', 'strict-all'],
1094
+ booleans: ['yes', 'force', 'deep', 'apply', 'help', 'version', 'global', 'project', 'no-hooks', 'mcp-json', 'no-origin', 'json', 'remote', 'local', 'link', 'archived', 'clear-ttl', 'telemetry', 'all', 'run', 'enabled', 'disabled', 'off', 'clear-min-age-days', 'clear-unseen-days', 'clear-max-seen-count', 'clear-max-read-count', 'strict', 'strict-all'],
1092
1095
  known: KNOWN_FLAGS,
1093
1096
  });
1094
1097
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.64.1",
3
+ "version": "1.65.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,6 +29,8 @@ export function parseGroomRequest(args) {
29
29
  if (unseen.error) return { error: unseen.error };
30
30
  const maxSeen = parseIntFlag(args['max-seen-count'], 'max-seen-count');
31
31
  if (maxSeen.error) return { error: maxSeen.error };
32
+ const maxRead = parseIntFlag(args['max-read-count'], 'max-read-count');
33
+ if (maxRead.error) return { error: maxRead.error };
32
34
 
33
35
  return {
34
36
  request: {
@@ -36,6 +38,7 @@ export function parseGroomRequest(args) {
36
38
  min_age_days: minAge.value,
37
39
  unseen_days: unseen.value,
38
40
  max_seen_count: maxSeen.value,
41
+ max_read_count: maxRead.value,
39
42
  },
40
43
  };
41
44
  }
@@ -30,6 +30,7 @@ function formatPolicy(p) {
30
30
  if (p.min_age_days != null) conditions.push(`min_age_days=${p.min_age_days}`);
31
31
  if (p.unseen_days != null) conditions.push(`unseen_days=${p.unseen_days}`);
32
32
  if (p.max_seen_count != null) conditions.push(`max_seen_count=${p.max_seen_count}`);
33
+ if (p.max_read_count != null) conditions.push(`max_read_count=${p.max_read_count}`);
33
34
  const mode = p.mode === 'auto' ? (p.enabled ? c.green('auto (enabled)') : c.dim('auto (disabled)')) : c.dim('review');
34
35
  return `${c.cyan(p.id)} ${c.bold(p.name)} ${c.dim(p.scope)} ${mode}${conditions.length ? ` ${c.dim(conditions.join(', '))}` : ''}`;
35
36
  }
@@ -50,7 +51,7 @@ async function list(args, store) {
50
51
 
51
52
  async function create(args, store) {
52
53
  if (!args.scope || !args.name) {
53
- err(`${c.red('Usage:')} lorekit policy create --scope <scope> --name <name> [--mode review|auto] [--enabled] [--min-age-days N] [--unseen-days N] [--max-seen-count N]`);
54
+ err(`${c.red('Usage:')} lorekit policy create --scope <scope> --name <name> [--mode review|auto] [--enabled] [--min-age-days N] [--unseen-days N] [--max-seen-count N] [--max-read-count N]`);
54
55
  return 1;
55
56
  }
56
57
  const minAge = parseIntFlag(args['min-age-days'], 'min-age-days');
@@ -59,6 +60,8 @@ async function create(args, store) {
59
60
  if (unseen.error) { err(`${c.red('Error:')} ${unseen.error}`); return 1; }
60
61
  const maxSeen = parseIntFlag(args['max-seen-count'], 'max-seen-count');
61
62
  if (maxSeen.error) { err(`${c.red('Error:')} ${maxSeen.error}`); return 1; }
63
+ const maxRead = parseIntFlag(args['max-read-count'], 'max-read-count');
64
+ if (maxRead.error) { err(`${c.red('Error:')} ${maxRead.error}`); return 1; }
62
65
  if (args.mode !== undefined && args.mode !== 'review' && args.mode !== 'auto') {
63
66
  err(`${c.red('Error:')} --mode must be "review" or "auto"`);
64
67
  return 1;
@@ -72,6 +75,7 @@ async function create(args, store) {
72
75
  min_age_days: minAge.value,
73
76
  unseen_days: unseen.value,
74
77
  max_seen_count: maxSeen.value,
78
+ max_read_count: maxRead.value,
75
79
  });
76
80
  if (!res.ok) {
77
81
  const msg = res.error?.message ?? res.error ?? res.networkError ?? 'the server rejected the request';
@@ -87,7 +91,7 @@ async function create(args, store) {
87
91
  async function update(args, store) {
88
92
  const id = args._[2];
89
93
  if (!id) {
90
- err(`${c.red('Usage:')} lorekit policy update <id> [--name N] [--mode review|auto] [--enabled|--disabled] [--min-age-days N] [--unseen-days N] [--max-seen-count N]`);
94
+ err(`${c.red('Usage:')} lorekit policy update <id> [--name N] [--mode review|auto] [--enabled|--disabled] [--min-age-days N] [--unseen-days N] [--max-seen-count N] [--max-read-count N]`);
91
95
  return 1;
92
96
  }
93
97
  if (args.mode !== undefined && args.mode !== 'review' && args.mode !== 'auto') {
@@ -104,6 +108,7 @@ async function update(args, store) {
104
108
  ['min-age-days', 'clear-min-age-days', 'min_age_days'],
105
109
  ['unseen-days', 'clear-unseen-days', 'unseen_days'],
106
110
  ['max-seen-count', 'clear-max-seen-count', 'max_seen_count'],
111
+ ['max-read-count', 'clear-max-read-count', 'max_read_count'],
107
112
  ]) {
108
113
  if (args[clearFlag]) { patch[field] = null; continue; }
109
114
  if (args[flag] === undefined) continue;
@@ -77,6 +77,7 @@ const FLAG = {
77
77
  'min-age-days': { desc: 'Match lessons at least n days old', arg: 'n' },
78
78
  'unseen-days': { desc: 'Match lessons unseen for at least n days', arg: 'n' },
79
79
  'max-seen-count': { desc: 'Match lessons that recurred at most n times', arg: 'n' },
80
+ 'max-read-count': { desc: 'Match lessons read at most n times (bulk reads count)', arg: 'n' },
80
81
  run: { desc: 'Archive the matches instead of previewing' },
81
82
  name: { desc: 'Policy name', arg: 'name' },
82
83
  enabled: { desc: 'Turn auto-mode on' },
@@ -84,6 +85,7 @@ const FLAG = {
84
85
  'clear-min-age-days': { desc: 'Remove the min-age-days condition' },
85
86
  'clear-unseen-days': { desc: 'Remove the unseen-days condition' },
86
87
  'clear-max-seen-count': { desc: 'Remove the max-seen-count condition' },
88
+ 'clear-max-read-count': { desc: 'Remove the max-read-count condition' },
87
89
  off: { desc: 'Unprotect instead of protect' },
88
90
  };
89
91
 
@@ -151,11 +153,11 @@ const COMMANDS = [
151
153
  flags: ['yes', 'json', 'endpoint', 'token'] },
152
154
  { name: 'groom', summary: 'Preview or run a retention sweep',
153
155
  values: { mode: ['review', 'auto'] },
154
- flags: ['policy-id', 'scope', 'min-age-days', 'unseen-days', 'max-seen-count', 'run', 'yes', 'json', 'endpoint', 'token'] },
156
+ flags: ['policy-id', 'scope', 'min-age-days', 'unseen-days', 'max-seen-count', 'max-read-count', 'run', 'yes', 'json', 'endpoint', 'token'] },
155
157
  { name: 'policy', summary: 'Manage saved retention rules',
156
158
  values: { mode: ['review', 'auto'] },
157
- flags: ['scope', 'name', 'mode', 'enabled', 'disabled', 'min-age-days', 'unseen-days', 'max-seen-count',
158
- 'clear-min-age-days', 'clear-unseen-days', 'clear-max-seen-count', 'yes', 'json', 'endpoint', 'token'] },
159
+ flags: ['scope', 'name', 'mode', 'enabled', 'disabled', 'min-age-days', 'unseen-days', 'max-seen-count', 'max-read-count',
160
+ 'clear-min-age-days', 'clear-unseen-days', 'clear-max-seen-count', 'clear-max-read-count', 'yes', 'json', 'endpoint', 'token'] },
159
161
  { name: 'protect', summary: 'Mark a memory protected, excluded from every grooming sweep', positional: 'address',
160
162
  flags: ['off', 'scope', 'key', 'json', 'endpoint', 'token'] },
161
163
  { name: 'pin', summary: 'Shorthand for `protect` (protected=true)', positional: 'address',
@@ -707,8 +707,8 @@ class RemoteStore {
707
707
  }
708
708
 
709
709
  // POST /policies → the created policy object.
710
- async policyCreate({ scope, name, mode, enabled, min_age_days, unseen_days, max_seen_count } = {}) {
711
- const body = stripUndefined({ scope, name, mode, enabled, min_age_days, unseen_days, max_seen_count });
710
+ async policyCreate({ scope, name, mode, enabled, min_age_days, unseen_days, max_seen_count, max_read_count } = {}) {
711
+ const body = stripUndefined({ scope, name, mode, enabled, min_age_days, unseen_days, max_seen_count, max_read_count });
712
712
  const res = await this._rest('/memories/policies', { method: 'POST', body });
713
713
  if (!res.ok) return { ok: false, error: res.error, httpStatus: res.httpStatus, networkError: res.networkError };
714
714
  return { ok: true, policy: res.data };
@@ -735,8 +735,8 @@ class RemoteStore {
735
735
  // POST /groom/preview → { count, keys: [{ scope, key }] } — the SAME
736
736
  // candidates a groom() run would archive. Pass either `policy_id` or
737
737
  // `scope` (+ optional conditions), never both.
738
- async groomPreview({ policy_id, scope, min_age_days, unseen_days, max_seen_count } = {}) {
739
- const body = stripUndefined({ policy_id, scope, min_age_days, unseen_days, max_seen_count });
738
+ async groomPreview({ policy_id, scope, min_age_days, unseen_days, max_seen_count, max_read_count } = {}) {
739
+ const body = stripUndefined({ policy_id, scope, min_age_days, unseen_days, max_seen_count, max_read_count });
740
740
  const res = await this._rest('/memories/groom/preview', { method: 'POST', body });
741
741
  if (!res.ok) return { ok: false, error: res.error, httpStatus: res.httpStatus, networkError: res.networkError };
742
742
  return { ok: true, count: res.data?.count ?? 0, keys: Array.isArray(res.data?.keys) ? res.data.keys : [] };
@@ -744,8 +744,8 @@ class RemoteStore {
744
744
 
745
745
  // POST /groom/run → archives every previewed candidate, in one transaction.
746
746
  // Soft-archive only (recoverable via restore); never hard-deletes.
747
- async groomRun({ policy_id, scope, min_age_days, unseen_days, max_seen_count } = {}) {
748
- const body = stripUndefined({ policy_id, scope, min_age_days, unseen_days, max_seen_count });
747
+ async groomRun({ policy_id, scope, min_age_days, unseen_days, max_seen_count, max_read_count } = {}) {
748
+ const body = stripUndefined({ policy_id, scope, min_age_days, unseen_days, max_seen_count, max_read_count });
749
749
  const res = await this._rest('/memories/groom/run', { method: 'POST', body });
750
750
  if (!res.ok) return { ok: false, error: res.error, httpStatus: res.httpStatus, networkError: res.networkError };
751
751
  return { ok: true, archived: res.data?.archived ?? 0, keys: Array.isArray(res.data?.keys) ? res.data.keys : [] };
@@ -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 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."
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 is measured from its creation date, so it matches only once it is itself this old."
525
525
  },
526
526
  "max_seen_count": {
527
527
  "type": "integer",
@@ -529,6 +529,12 @@ export const MCP_TOOL_DEFS = [
529
529
  "maximum": 100000,
530
530
  "description": "Match only lessons that have recurred at most this many times."
531
531
  },
532
+ "max_read_count": {
533
+ "type": "integer",
534
+ "minimum": 0,
535
+ "maximum": 100000,
536
+ "description": "Match only lessons READ at most this many times — the counter that says whether a lesson was ever actually used, unlike `max_seen_count` which counts WRITES. Counts EVERY read, a bulk `memory.list`/`memory.search` appearance included (unlike `unseen_days`, which only counts targeted opens). Reads have only been counted since the counter shipped, so a long-lived lesson can show a low count it never earned."
537
+ },
532
538
  "tags": {
533
539
  "type": "array",
534
540
  "items": {
@@ -700,7 +706,7 @@ export const MCP_TOOL_DEFS = [
700
706
  "type": "integer",
701
707
  "minimum": 1,
702
708
  "maximum": 3650,
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."
709
+ "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 is measured from its creation date. Omit to leave unchanged; pass explicit null to clear."
704
710
  },
705
711
  "max_seen_count": {
706
712
  "type": "integer",
@@ -708,6 +714,12 @@ export const MCP_TOOL_DEFS = [
708
714
  "maximum": 100000,
709
715
  "description": "Match only lessons that have recurred at most this many times. Omit to leave unchanged; pass explicit null to clear."
710
716
  },
717
+ "max_read_count": {
718
+ "type": "integer",
719
+ "minimum": 0,
720
+ "maximum": 100000,
721
+ "description": "Match only lessons READ at most this many times — the counter that says whether a lesson was ever actually used, unlike `max_seen_count` which counts WRITES. Counts EVERY read, a bulk `memory.list`/`memory.search` appearance included (unlike `unseen_days`, which only counts targeted opens). Reads have only been counted since the counter shipped, so a long-lived lesson can show a low count it never earned. Omit to leave unchanged; pass explicit null to clear."
722
+ },
711
723
  "tags": {
712
724
  "type": "array",
713
725
  "items": {
@@ -880,7 +892,7 @@ export const MCP_TOOL_DEFS = [
880
892
  "type": "integer",
881
893
  "minimum": 1,
882
894
  "maximum": 3650,
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."
895
+ "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 is measured from its creation date, so it matches only once it is itself this old."
884
896
  },
885
897
  "max_seen_count": {
886
898
  "type": "integer",
@@ -888,6 +900,12 @@ export const MCP_TOOL_DEFS = [
888
900
  "maximum": 100000,
889
901
  "description": "Match only lessons that have recurred at most this many times."
890
902
  },
903
+ "max_read_count": {
904
+ "type": "integer",
905
+ "minimum": 0,
906
+ "maximum": 100000,
907
+ "description": "Match only lessons READ at most this many times — the counter that says whether a lesson was ever actually used, unlike `max_seen_count` which counts WRITES. Counts EVERY read, a bulk `memory.list`/`memory.search` appearance included (unlike `unseen_days`, which only counts targeted opens). Reads have only been counted since the counter shipped, so a long-lived lesson can show a low count it never earned."
908
+ },
891
909
  "tags": {
892
910
  "type": "array",
893
911
  "items": {
@@ -1044,7 +1062,7 @@ export const MCP_TOOL_DEFS = [
1044
1062
  "type": "integer",
1045
1063
  "minimum": 1,
1046
1064
  "maximum": 3650,
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."
1065
+ "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 is measured from its creation date, so it matches only once it is itself this old."
1048
1066
  },
1049
1067
  "max_seen_count": {
1050
1068
  "type": "integer",
@@ -1052,6 +1070,12 @@ export const MCP_TOOL_DEFS = [
1052
1070
  "maximum": 100000,
1053
1071
  "description": "Match only lessons that have recurred at most this many times."
1054
1072
  },
1073
+ "max_read_count": {
1074
+ "type": "integer",
1075
+ "minimum": 0,
1076
+ "maximum": 100000,
1077
+ "description": "Match only lessons READ at most this many times — the counter that says whether a lesson was ever actually used, unlike `max_seen_count` which counts WRITES. Counts EVERY read, a bulk `memory.list`/`memory.search` appearance included (unlike `unseen_days`, which only counts targeted opens). Reads have only been counted since the counter shipped, so a long-lived lesson can show a low count it never earned."
1078
+ },
1055
1079
  "tags": {
1056
1080
  "type": "array",
1057
1081
  "items": {