@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
|
@@ -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'); }
|
package/src/store/remote.mjs
CHANGED
|
@@ -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
|