@lorekit/cli 1.57.0 → 1.58.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.57.0",
3
+ "version": "1.58.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": {
@@ -1,5 +1,8 @@
1
1
  // Minimal MCP-over-HTTP (JSON-RPC 2.0) client for the LoreKit endpoint.
2
- // Zero dependencies — uses the global fetch (Node 18+).
2
+ // Zero EXTERNAL dependencies — uses the global fetch (Node 18+). Imports
3
+ // below are same-package sibling modules (`./origin.mjs`), not npm deps.
4
+
5
+ import { prNumberFromEnv, isValidRepo } from './origin.mjs';
3
6
 
4
7
  // Split a configured server URL like ".../mcp?token=lk_rw_x" into
5
8
  // { endpoint: ".../mcp", token: "lk_rw_x" }.
@@ -170,6 +173,94 @@ export function normalizeCorrelationId(raw) {
170
173
  return /^[A-Za-z0-9_\-./:#@]+$/.test(t) ? t : null;
171
174
  }
172
175
 
176
+ /**
177
+ * The bounded `session_kind` vocabulary (migration 00082) — sent via
178
+ * `X-LoreKit-Session-Kind`, validated edge-side by the CROSS-LANGUAGE twin of
179
+ * this file's derivation, `packages/mcp-core/src/telemetry/session-kind.ts`
180
+ * (`parseSessionKind`). Kept here rather than imported: this package has no
181
+ * dependency on `@lorekit/core`, and the two are guarded for behavioural
182
+ * parity by `session-kind-parity.spec.ts` rather than a byte comparison,
183
+ * which is what a cross-language pair (this `.mjs` vs that `.ts`) needs.
184
+ */
185
+ const SESSION_KINDS = ['local', 'ci', 'pr', 'unknown'];
186
+
187
+ /**
188
+ * Derive `{ correlationId, sessionKind }` from the ambient environment, for
189
+ * every call site that does not have an EXPLICIT `LOREKIT_CORRELATION_ID` —
190
+ * the caller checks that first and skips this entirely when it is set, since
191
+ * an explicit value always wins.
192
+ *
193
+ * Precedence, first match wins:
194
+ * 1. PR context — `prNumberFromEnv` (LOREKIT_PR / GITHUB_REF / GITHUB_PR_NUMBER,
195
+ * see `origin.mjs`) resolves a PR number AND a repo is known → `pr` +
196
+ * `pr:<owner>/<repo>#<n>`.
197
+ * 2. CI environment (`GITHUB_ACTIONS`/`CI`) — `ci` always; a correlation id
198
+ * of `ci:<owner>/<repo>#<run_id>` when both a repo and GITHUB_RUN_ID are
199
+ * known, otherwise no correlation id (still `ci` — the session KIND is
200
+ * known even when a stable id to group by is not).
201
+ * 3. A host-provided session id (`LOREKIT_SESSION_ID`, or the handful of
202
+ * well-known agent-host env vars below) — `local` +
203
+ * `session:<id>`. The raw id itself is never logged or stored anywhere
204
+ * beyond this derived correlation id.
205
+ * 4. Otherwise `unknown`, no correlation id — never a guess.
206
+ *
207
+ * TOTAL and fail-safe: reads only `env` (never throws on a missing/odd
208
+ * value), and every branch degrades to the next rather than throwing. A
209
+ * derived value that fails `normalizeCorrelationId`'s charset/length check is
210
+ * dropped (session_kind is still reported; only the drill-down id is not).
211
+ */
212
+ export function deriveSessionContext(env = process.env) {
213
+ const repo = isValidRepo(env.GITHUB_REPOSITORY);
214
+ const prNumber = prNumberFromEnv(env);
215
+
216
+ if (prNumber !== null && repo) {
217
+ const correlationId = normalizeCorrelationId(`pr:${repo}#${prNumber}`);
218
+ if (correlationId) return { correlationId, sessionKind: 'pr' };
219
+ }
220
+
221
+ const isCI = env.GITHUB_ACTIONS === 'true' || env.CI === 'true' || env.CI === '1';
222
+ if (isCI) {
223
+ const runId = typeof env.GITHUB_RUN_ID === 'string' ? env.GITHUB_RUN_ID.trim() : '';
224
+ if (repo && runId) {
225
+ const correlationId = normalizeCorrelationId(`ci:${repo}#${runId}`);
226
+ if (correlationId) return { correlationId, sessionKind: 'ci' };
227
+ }
228
+ return { correlationId: null, sessionKind: 'ci' };
229
+ }
230
+
231
+ // Well-known agent-host session id env vars. Best-effort: hosts differ and
232
+ // this is not an exhaustive registry, so an unrecognised host still falls
233
+ // through to `unknown` rather than fabricating an id.
234
+ const sessionId = firstNonEmptyEnv(env, ['LOREKIT_SESSION_ID', 'CLAUDE_SESSION_ID']);
235
+ if (sessionId) {
236
+ // A local session IS known even when the specific id fails the
237
+ // correlation-id charset/length check — report the kind either way, and
238
+ // let the id itself degrade to null rather than losing the whole reading.
239
+ return { correlationId: normalizeCorrelationId(`session:${sessionId}`), sessionKind: 'local' };
240
+ }
241
+
242
+ return { correlationId: null, sessionKind: 'unknown' };
243
+ }
244
+
245
+ function firstNonEmptyEnv(env, keys) {
246
+ for (const key of keys) {
247
+ const v = env[key];
248
+ if (typeof v === 'string' && v.trim() !== '') return v.trim();
249
+ }
250
+ return null;
251
+ }
252
+
253
+ /**
254
+ * Validate a `session_kind` value against the closed vocabulary. Total and
255
+ * fail-safe — mirrors `parseSessionKind`'s behaviour (never used to VALIDATE
256
+ * an incoming value here, since this process only ever sends a value it just
257
+ * derived itself, but kept as the single place the vocabulary is spelled out
258
+ * so `deriveSessionContext` and any future caller cannot drift from it).
259
+ */
260
+ export function isSessionKind(value) {
261
+ return SESSION_KINDS.includes(value);
262
+ }
263
+
173
264
  /**
174
265
  * Normalise a deployment-environment marker restFetch attaches as
175
266
  * X-LoreKit-Deployment-Environment when DEPLOYMENT_ENVIRONMENT (or
@@ -219,11 +310,17 @@ export async function restFetch(baseUrl, token, path, { method = 'GET', body, ti
219
310
  const timer = setTimeout(() => controller.abort(), timeoutMs);
220
311
  try {
221
312
  const url = `${baseUrl}${path}`;
222
- // Opt-in usage correlation: when LOREKIT_CORRELATION_ID is set (e.g. by a CI
223
- // job or a hook to a PR/session id), tag every REST call so GET
224
- // /memories/usage?correlation_id=… can report "usage for this PR". Absent env
225
- // no header existing behaviour unchanged.
226
- const correlationId = normalizeCorrelationId(process.env.LOREKIT_CORRELATION_ID);
313
+ // Usage correlation: an EXPLICIT LOREKIT_CORRELATION_ID always wins (e.g. a
314
+ // CI job or a hook hand-setting a PR/session id). Otherwise, derive one
315
+ // from the ambient environment (CI/PR/session see `deriveSessionContext`)
316
+ // so GET /memories/usage?correlation_id=… and the session_kind dimension
317
+ // are populated without anyone having to export anything by hand. Both
318
+ // stay unset only when neither an explicit value nor a derivable one
319
+ // exists (`sessionKind: 'unknown'`, no correlationId).
320
+ const explicitCorrelationId = normalizeCorrelationId(process.env.LOREKIT_CORRELATION_ID);
321
+ const derived = explicitCorrelationId ? null : deriveSessionContext(process.env);
322
+ const correlationId = explicitCorrelationId ?? derived?.correlationId ?? null;
323
+ const sessionKind = derived?.sessionKind ?? null;
227
324
  // Opt-in test-run marker: when DEPLOYMENT_ENVIRONMENT is set (a deploy/CI
228
325
  // smoke sets it to `test`), tell the edge to report that
229
326
  // `deployment.environment.name` for this request so Dash0 can filter synthetic
@@ -238,6 +335,7 @@ export async function restFetch(baseUrl, token, path, { method = 'GET', body, ti
238
335
  ...(token ? { authorization: `Bearer ${token}` } : {}),
239
336
  ...(traceparent ? { traceparent } : {}),
240
337
  ...(correlationId ? { 'x-lorekit-correlation-id': correlationId } : {}),
338
+ ...(sessionKind ? { 'x-lorekit-session-kind': sessionKind } : {}),
241
339
  ...(runEnv ? { 'x-lorekit-deployment-environment': runEnv } : {}),
242
340
  // Name the calling surface so usage analytics can tell a CLI read from a
243
341
  // dashboard one. Not cosmetic: `GET /memories/read-activity` EXCLUDES the
@@ -54,6 +54,12 @@ export const mirrorPairs = [
54
54
  { core: 'packages/mcp-core/src/webhook/github-app-jwt.ts', edge: 'supabase/functions/mcp/github-app-jwt.ts', driftChecked: true },
55
55
  { core: 'packages/mcp-core/src/telemetry/trace-context.ts', edge: 'supabase/functions/_shared/telemetry/trace-context.ts', driftChecked: true },
56
56
  { core: 'packages/mcp-core/src/rest/rest-tool-name.ts', edge: 'supabase/functions/_shared/rest/rest-tool-name.ts', driftChecked: true },
57
+ // The `X-LoreKit-Session-Kind` validator (migration 00082). Also has a
58
+ // SECOND, cross-LANGUAGE twin — the CLI's `deriveSessionContext` in
59
+ // `packages/cli/src/shared/mcp.mjs` — guarded behaviourally by
60
+ // `packages/cli/test/session-context.test.mjs`, the same split
61
+ // `lesson-rank.ts` uses below.
62
+ { core: 'packages/mcp-core/src/telemetry/session-kind.ts', edge: 'supabase/functions/_shared/telemetry/session-kind.ts', driftChecked: true },
57
63
  // Has a SECOND, cross-LANGUAGE twin no byte comparison can cover — the
58
64
  // CLI's own `lessons-pure.mjs` — guarded behaviourally by
59
65
  // `lesson-rank-parity.spec.ts` instead.