@lorekit/cli 1.67.0 → 1.69.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 +29 -8
- package/package.json +1 -1
- package/skill/lorekit-groom/rules/grooming-pass.md +10 -0
- package/skill/lorekit-memory/SKILL.md +26 -3
- package/skill/lorekit-memory/references/scope-resolution.md +23 -1
- package/skill/lorekit-memory/rules/intake.md +41 -0
- package/skill/lorekit-memory/rules/retrospective.md +7 -0
- package/skill/lorekit-setup/SKILL.md +42 -1
- package/skill/lorekit-setup/rules/ci-state-records.md +1 -1
- package/skill/lorekit-setup/rules/compiled-invariants.md +9 -8
- package/skill/lorekit-setup/rules/self-improvement-loops.md +278 -37
- package/src/commands/invariants.mjs +17 -10
- package/src/commands/show.mjs +244 -3
- package/src/shared/candidates-pure.mjs +94 -5
- package/src/shared/mirror-pairs.mjs +9 -0
- package/src/shared/scope-precedence.mjs +82 -0
- package/src/store/local.mjs +95 -1
- package/src/store/remote.mjs +60 -3
- package/src/surfaces.generated.mjs +14 -17
package/src/store/remote.mjs
CHANGED
|
@@ -22,8 +22,19 @@ import { restFetch, mcpToRestBase } from '../shared/mcp.mjs';
|
|
|
22
22
|
import { rememberAccountId } from '../telemetry/telemetry-identity.mjs';
|
|
23
23
|
import { getActiveTraceparent } from '../telemetry/telemetry.mjs';
|
|
24
24
|
import { withReadFields } from './entry-fields.mjs';
|
|
25
|
+
import { pickScopeWinner } from '../shared/scope-precedence.mjs';
|
|
25
26
|
import { normalizeCreatedAt } from './created-at.mjs';
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* How many same-key rows an UNSCOPED read fetches before picking a winner.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately the same number the hosted `memory.read` caps its own candidate
|
|
32
|
+
* fan-out at, so a key living in a pathological number of scopes truncates at
|
|
33
|
+
* the same point on both surfaces instead of the two considering different
|
|
34
|
+
* candidate sets and disagreeing about the winner.
|
|
35
|
+
*/
|
|
36
|
+
const UNSCOPED_READ_CANDIDATE_LIMIT = 50;
|
|
37
|
+
|
|
27
38
|
// Drop undefined/null args so JSON payloads stay tidy.
|
|
28
39
|
function stripUndefined(obj) {
|
|
29
40
|
const out = {};
|
|
@@ -249,12 +260,24 @@ class RemoteStore {
|
|
|
249
260
|
};
|
|
250
261
|
}
|
|
251
262
|
|
|
263
|
+
// read({ scope, key }) → { ok, entry } — `scope` is OPTIONAL.
|
|
264
|
+
//
|
|
265
|
+
// Without one the key can match once per scope holding it, so the precedence
|
|
266
|
+
// pick happens HERE rather than server-side: `GET /memories` is a LIST route,
|
|
267
|
+
// and narrowing it to a single winner would change what every other caller of
|
|
268
|
+
// it gets back. The rule is the shared `scope-precedence` one, so this
|
|
269
|
+
// resolves identically to MCP's `memory.read` (which picks server-side
|
|
270
|
+
// because it must answer with one lesson) and to the offline store.
|
|
252
271
|
async read({ scope, key } = {}) {
|
|
253
272
|
const p = new URLSearchParams();
|
|
254
273
|
if (scope) p.set('scope', scope);
|
|
255
274
|
if (key) p.set('key', key);
|
|
256
|
-
// scope
|
|
257
|
-
|
|
275
|
+
// With a scope, `scope`+`key` is unique and one row is all there can be —
|
|
276
|
+
// don't pull the default page of 50. Without one, fetch the candidates the
|
|
277
|
+
// precedence pick needs; the cap matches the hosted read's own so the two
|
|
278
|
+
// surfaces truncate a pathological key at the same point rather than
|
|
279
|
+
// disagreeing about which lessons were even considered.
|
|
280
|
+
p.set('limit', scope ? '1' : String(UNSCOPED_READ_CANDIDATE_LIMIT));
|
|
258
281
|
const res = await this._rest(`/memories?${p}`);
|
|
259
282
|
// `unusable` is passed through: `_rest` short-circuits an unconfigured
|
|
260
283
|
// store with that flag and NOTHING else, so a caller that drops it is left
|
|
@@ -275,9 +298,43 @@ class RemoteStore {
|
|
|
275
298
|
};
|
|
276
299
|
}
|
|
277
300
|
const entries = res.data?.entries ?? [];
|
|
301
|
+
// `key` is an EXACT filter server-side, so every row here carries the key
|
|
302
|
+
// that was asked for; precedence only has to choose among their scopes. A
|
|
303
|
+
// scoped read has at most one candidate, so the pick is a no-op there.
|
|
304
|
+
const winner = pickScopeWinner(entries);
|
|
278
305
|
// Same projection as list/search — a single read must not answer with a
|
|
279
306
|
// different shape than the listing the caller found the key in.
|
|
280
|
-
return { ok: true, entry:
|
|
307
|
+
return { ok: true, entry: winner ? withReadFields(winner) : null };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// readMany(refs) → { ok, entries, missing } — `POST /memories/read`, one
|
|
311
|
+
// round-trip for the whole batch (the server groups by scope and issues one
|
|
312
|
+
// `.in('key', …)` query per distinct scope — see `handlers/read.ts`). `refs`
|
|
313
|
+
// is an array of `{ scope, key }`; this transport joins each into the
|
|
314
|
+
// `scope::key` wire form the route's `ReadMemoriesBodySchema` expects. An
|
|
315
|
+
// empty/absent list short-circuits locally rather than making a request the
|
|
316
|
+
// server would just 400 on (`refs` has `.min(1)`).
|
|
317
|
+
async readMany(refs) {
|
|
318
|
+
const list = Array.isArray(refs) ? refs.filter(Boolean) : [];
|
|
319
|
+
if (!list.length) return { ok: true, entries: [], missing: [] };
|
|
320
|
+
const body = { refs: list.map((r) => `${r.scope}::${r.key}`) };
|
|
321
|
+
const res = await this._rest('/memories/read', { method: 'POST', body });
|
|
322
|
+
if (!res.ok) {
|
|
323
|
+
return {
|
|
324
|
+
ok: false,
|
|
325
|
+
error: res.error ?? null,
|
|
326
|
+
httpStatus: res.httpStatus ?? null,
|
|
327
|
+
retryAfter: res.retryAfter ?? null,
|
|
328
|
+
networkError: res.networkError ?? null,
|
|
329
|
+
unusable: res.unusable ?? false,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const data = res.data ?? {};
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
entries: (data.entries ?? []).map(withReadFields),
|
|
336
|
+
missing: data.missing ?? [],
|
|
337
|
+
};
|
|
281
338
|
}
|
|
282
339
|
|
|
283
340
|
async write(args = {}) {
|
|
@@ -160,37 +160,37 @@ export const MCP_TOOL_DEFS = [
|
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
"name": "memory.read",
|
|
163
|
-
"description": "Read
|
|
163
|
+
"description": "Read one lesson by `key`, or several at once by `refs`. Pass exactly one of those two shapes: `key` (optionally narrowed with `scope`), or `refs` alone — a call carrying both is rejected, and so is one carrying neither. `scope` is optional: omit it and the key is resolved across every scope you can see, preferring the most specific one.",
|
|
164
164
|
"inputSchema": {
|
|
165
165
|
"type": "object",
|
|
166
|
-
"required": [
|
|
167
|
-
"scope",
|
|
168
|
-
"key"
|
|
169
|
-
],
|
|
170
166
|
"properties": {
|
|
171
167
|
"scope": {
|
|
172
168
|
"type": "string",
|
|
173
|
-
"description": "Canonical scope string, e.g. `repo::mthines/lorekit`."
|
|
169
|
+
"description": "Canonical scope string, e.g. `repo::mthines/lorekit`. OPTIONAL — omit it to search every scope you can see."
|
|
174
170
|
},
|
|
175
171
|
"key": {
|
|
176
172
|
"type": "string",
|
|
177
173
|
"description": "Lesson identifier, unique within the scope. Max 512 characters."
|
|
174
|
+
},
|
|
175
|
+
"refs": {
|
|
176
|
+
"type": "array",
|
|
177
|
+
"items": {
|
|
178
|
+
"type": "string"
|
|
179
|
+
},
|
|
180
|
+
"description": "Batch mode: one or more `scope::key` references, fetched in a single call. Cannot be combined with `scope`/`key`. Each entry is parsed by the same reference grammar `memory.write`'s `cited` field uses (`scope::key`, verbatim scope — never lowercased). Silently truncated past 32 entries."
|
|
178
181
|
}
|
|
179
182
|
}
|
|
180
183
|
}
|
|
181
184
|
},
|
|
182
185
|
{
|
|
183
186
|
"name": "memory.list",
|
|
184
|
-
"description": "List lessons for
|
|
187
|
+
"description": "List lessons, for one scope or across every scope you can see",
|
|
185
188
|
"inputSchema": {
|
|
186
189
|
"type": "object",
|
|
187
|
-
"required": [
|
|
188
|
-
"scope"
|
|
189
|
-
],
|
|
190
190
|
"properties": {
|
|
191
191
|
"scope": {
|
|
192
192
|
"type": "string",
|
|
193
|
-
"description": "Canonical scope string, e.g. `repo::mthines/lorekit`."
|
|
193
|
+
"description": "Canonical scope string, e.g. `repo::mthines/lorekit`. OPTIONAL — omit it to search every scope you can see."
|
|
194
194
|
},
|
|
195
195
|
"tags": {
|
|
196
196
|
"type": "array",
|
|
@@ -338,7 +338,7 @@ export const MCP_TOOL_DEFS = [
|
|
|
338
338
|
},
|
|
339
339
|
{
|
|
340
340
|
"name": "memory.scopes",
|
|
341
|
-
"description": "List every scope in the store with how many active memories it holds and when it was last written to — the inventory to consult when you do not already know which scope to read. Takes no arguments and is store-wide, NOT limited to any working directory.
|
|
341
|
+
"description": "List every scope in the store with how many active memories it holds and when it was last written to — the inventory to consult when you do not already know which scope to read. Takes no arguments and is store-wide, NOT limited to any working directory. The other read tools answer questions about lore; this one answers \"what is there?\" — reach for it when you want to NAME a scope, to narrow a list or to decide where a write belongs.",
|
|
342
342
|
"inputSchema": {
|
|
343
343
|
"type": "object",
|
|
344
344
|
"properties": {}
|
|
@@ -346,16 +346,13 @@ export const MCP_TOOL_DEFS = [
|
|
|
346
346
|
},
|
|
347
347
|
{
|
|
348
348
|
"name": "memory.list_archived",
|
|
349
|
-
"description": "List archived (soft-deleted) lessons for
|
|
349
|
+
"description": "List archived (soft-deleted) lessons, for one scope or across every scope you can see",
|
|
350
350
|
"inputSchema": {
|
|
351
351
|
"type": "object",
|
|
352
|
-
"required": [
|
|
353
|
-
"scope"
|
|
354
|
-
],
|
|
355
352
|
"properties": {
|
|
356
353
|
"scope": {
|
|
357
354
|
"type": "string",
|
|
358
|
-
"description": "Canonical scope string, e.g. `repo::mthines/lorekit`."
|
|
355
|
+
"description": "Canonical scope string, e.g. `repo::mthines/lorekit`. OPTIONAL — omit it to search every scope you can see."
|
|
359
356
|
},
|
|
360
357
|
"limit": {
|
|
361
358
|
"type": "integer",
|