@lorekit/cli 1.52.2 → 1.53.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
@@ -10,6 +10,7 @@ import { list } from '../src/list.mjs';
10
10
  import { search } from '../src/search.mjs';
11
11
  import { show } from '../src/show.mjs';
12
12
  import { write } from '../src/write.mjs';
13
+ import { archive, del, restore } from '../src/remove.mjs';
13
14
  import { stats } from '../src/stats.mjs';
14
15
  import { scopes } from '../src/scopes.mjs';
15
16
  import { diff } from '../src/diff.mjs';
@@ -698,11 +699,12 @@ const KNOWN_FLAGS = [
698
699
  const HUMAN_COMMANDS = new Set([
699
700
  'install', 'uninstall', 'doctor', 'list', 'search', 'show', 'stats', 'scopes',
700
701
  'diff', 'tree', 'lint', 'dedupe', 'link', 'migrate', 'write',
702
+ 'archive', 'delete', 'restore',
701
703
  ]);
702
704
 
703
705
  // Command aliases — canonicalized before help / dispatch so `lorekit ls --help`
704
706
  // and telemetry both resolve to the real command name.
705
- const COMMAND_ALIASES = { ls: 'list', grep: 'search', resolve: 'tree', url: 'link' };
707
+ const COMMAND_ALIASES = { ls: 'list', grep: 'search', resolve: 'tree', url: 'link', rm: 'delete' };
706
708
 
707
709
  async function main() {
708
710
  // Load a `.env` from the current directory (if any) before anything reads the
@@ -797,6 +799,12 @@ async function main() {
797
799
  return traceCommand('bootstrap', args, VERSION, () => bootstrap(args));
798
800
  case 'write':
799
801
  return traceCommand('write', args, VERSION, () => write(args));
802
+ case 'archive':
803
+ return traceCommand('archive', args, VERSION, () => archive(args));
804
+ case 'delete':
805
+ return traceCommand('delete', args, VERSION, () => del(args));
806
+ case 'restore':
807
+ return traceCommand('restore', args, VERSION, () => restore(args));
800
808
  default:
801
809
  err(`${c.red('Unknown command:')} ${command}\n`);
802
810
  log(HELP);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.52.2",
3
+ "version": "1.53.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": {
@@ -62,4 +62,8 @@ Wildcards work **only** in `memory.search` — not in `memory.list`,
62
62
  2. `repo::` must include a `/` (owner/repo); `repo::mthines` → 400.
63
63
  3. `branch::` must have exactly two `::` separators.
64
64
  4. Only `global`, `project`, `repo`, `branch` prefixes are valid.
65
- 5. Segments are trimmed and lowercased on ingest.
65
+ 5. Segments are trimmed and lowercased on ingest by the MCP tools (`memory.write`
66
+ validates through the normalising `validateScope`). The REST write path
67
+ (`POST /memories`) stores the scope EXACTLY as sent, so `memories.scope` can
68
+ hold mixed case — which is why a `?scope=` filter on the `/memories` routes is
69
+ validated but never lowercased and matches the stored string exactly.
package/src/remove.mjs ADDED
@@ -0,0 +1,113 @@
1
+ // `lorekit archive|delete|restore <scope::key>` — the removal lifecycle from the
2
+ // CLI, the counterpart to `write`.
3
+ //
4
+ // archive <scope::key> soft-archive (hide, reversible)
5
+ // delete <scope::key> [--force] soft-archive, or hard-delete with --force
6
+ // restore <scope::key> un-archive a soft-archived memory
7
+ //
8
+ // All three address a memory the same `<scope::key>` way `write`/`list`/`show`
9
+ // do (or the explicit `<scope> <key>` / `--scope --key` forms), and pick a store
10
+ // with the same precedence `write` uses (remote when usable, else local; forced
11
+ // by --remote / --local). Server-side these map to the hosted memory.archive /
12
+ // memory.delete / memory.restore operations, which are scope-authorized for an
13
+ // API token by its allowlist (migrations 00071 / 00072): a key scoped to a
14
+ // scope may manage every writer's row in it, an unscoped key only its own.
15
+ import { resolveProjectRoot } from './config.mjs';
16
+ import { loadControl, resolveDenies } from './control.mjs';
17
+ import { resolveStores, remoteUnavailableReason } from './stores.mjs';
18
+ import { log, err, c } from './util.mjs';
19
+ import { resolveScopeKeyArgs, scopeIssue } from './lessons-view.mjs';
20
+
21
+ // The three surfaces return different success shapes: the remote store answers
22
+ // `{ ok, error, networkError }`, the local store `{ deleted, archived, entry }`.
23
+ // Treat any non-error positive signal as success, and a bare `ok: false` with no
24
+ // error as "nothing matched".
25
+ function outcome(res, op) {
26
+ if (!res) return { ok: false, error: 'no response from store' };
27
+ if (res.networkError) return { ok: false, error: res.networkError };
28
+ if (res.error) return { ok: false, error: res.error };
29
+ const changed =
30
+ res.ok === true ||
31
+ Boolean(res.entry) ||
32
+ (op === 'restore' ? res.restored === true : res.deleted === true || res.archived === true);
33
+ return { ok: changed, error: changed ? null : null };
34
+ }
35
+
36
+ function pickStore({ root, env, args }) {
37
+ const { localDenied, remoteDenied } = resolveDenies(root, { env });
38
+ const { local, remote, connection } = resolveStores(root, {
39
+ env,
40
+ endpoint: args.endpoint,
41
+ token: args.token,
42
+ });
43
+ const forceRemote = Boolean(args.remote);
44
+ const forceLocal = Boolean(args.local);
45
+ if (forceRemote && forceLocal) return { error: '--remote and --local are mutually exclusive' };
46
+
47
+ if (forceRemote) {
48
+ if (remoteDenied) return { error: `remote store is disabled by deny constraint (${remoteDenied.source})` };
49
+ if (!remote.usable()) return { error: `remote store is not configured — ${remoteUnavailableReason(connection)}` };
50
+ return { store: remote, storeName: 'remote' };
51
+ }
52
+ if (forceLocal) {
53
+ if (localDenied) return { error: `local store is disabled by deny constraint (${localDenied.source})` };
54
+ return { store: local, storeName: 'local' };
55
+ }
56
+ if (!remoteDenied && remote.usable()) return { store: remote, storeName: 'remote' };
57
+ if (!localDenied) return { store: local, storeName: 'local' };
58
+ return { error: `no writable store available — ${remoteUnavailableReason(connection)}` };
59
+ }
60
+
61
+ // One implementation for all three verbs; `op` selects the store method, the
62
+ // past-tense word, and (for delete) whether --force hard-deletes.
63
+ async function run(args, op) {
64
+ const root = resolveProjectRoot(args.dir);
65
+ const env = process.env;
66
+ loadControl(root, { env });
67
+
68
+ const positionals = args._.slice(1);
69
+ const { scope, key } = resolveScopeKeyArgs(positionals, { scope: args.scope, key: args.key });
70
+
71
+ const badScope = scope ? scopeIssue(scope) : null;
72
+ if (badScope) {
73
+ err(`${c.red('Error:')} invalid scope ${c.cyan(scope)} — ${badScope}`);
74
+ return 1;
75
+ }
76
+ if (!scope || !key) {
77
+ err(`${c.red('Usage:')} lorekit ${op} <scope::key>`);
78
+ err(` lorekit ${op} <scope> <key>`);
79
+ err(`A scope and a key are required. Run ${c.cyan(`lorekit ${op} --help`)} for options.`);
80
+ return 1;
81
+ }
82
+
83
+ const picked = pickStore({ root, env, args });
84
+ if (picked.error) {
85
+ err(`${c.red('Error:')} ${picked.error}`);
86
+ return 1;
87
+ }
88
+ const { store, storeName } = picked;
89
+
90
+ const force = op === 'delete' && Boolean(args.force);
91
+ let res;
92
+ if (op === 'archive') res = await store.archive({ scope, key });
93
+ else if (op === 'restore') res = await store.restore({ scope, key });
94
+ else res = await store.delete({ scope, key, force });
95
+
96
+ const { ok, error } = outcome(res, op);
97
+ const past = op === 'archive' ? 'archived' : op === 'restore' ? 'restored' : force ? 'deleted' : 'archived';
98
+
99
+ if (args.json) {
100
+ log(JSON.stringify({ ok, store: storeName, scope, key, op, force, error }, null, 2));
101
+ return ok ? 0 : 1;
102
+ }
103
+ if (!ok) {
104
+ err(`${c.red('Error:')} could not ${op} ${c.cyan(`${scope}::${key}`)}${error ? ` — ${error}` : ' — no matching memory (or not permitted for this token)'}`);
105
+ return 1;
106
+ }
107
+ log(`${c.green('✓')} ${past} ${c.cyan(`${scope}::${key}`)} ${c.dim(`(${storeName})`)}`);
108
+ return 0;
109
+ }
110
+
111
+ export function archive(args) { return run(args, 'archive'); }
112
+ export function del(args) { return run(args, 'delete'); }
113
+ export function restore(args) { return run(args, 'restore'); }
@@ -508,6 +508,14 @@ class RemoteStore {
508
508
  return this.delete({ scope, key, force: false });
509
509
  }
510
510
 
511
+ // Natural-key RESTORE — un-archive a soft-deleted memory. POST
512
+ // /memories/restore with {scope, key} (supabase/functions/memories/handlers/
513
+ // restore.ts). Symmetric with archive; scope-authorized server-side (00072).
514
+ async restore({ scope, key } = {}) {
515
+ const res = await this._rest('/memories/restore', { method: 'POST', body: { scope, key } });
516
+ return { ok: res.ok, error: res.error, networkError: res.networkError };
517
+ }
518
+
511
519
  // ── Org operations → REST ─────────────────────────────────────────────────
512
520
  // `supabase/functions/orgs/` serves `lk_*` tokens on every route as of
513
521
  // 00041_org_actor_override.sql (see the file header). Each method's RETURN