@lorekit/cli 1.5.0 → 1.7.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.5.0",
3
+ "version": "1.7.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": {
package/src/doctor.mjs CHANGED
@@ -24,8 +24,9 @@ export async function doctor(args) {
24
24
  const root = resolveProjectRoot(args.dir);
25
25
  let failures = 0;
26
26
  let warnings = 0;
27
+ const failedChecks = [];
27
28
  const record = (kind, label, detail) => {
28
- if (kind === 'fail') failures++;
29
+ if (kind === 'fail') { failures++; failedChecks.push(label); }
29
30
  if (kind === 'warn') warnings++;
30
31
  status(kind, label, detail);
31
32
  };
@@ -91,7 +92,8 @@ export async function doctor(args) {
91
92
  }.`,
92
93
  );
93
94
  }
94
- return failures === 0 ? 0 : 1;
95
+ const exitCode = failures === 0 ? 0 : 1;
96
+ return { exitCode, 'lorekit.cli.doctor.failed_checks': failedChecks };
95
97
  }
96
98
 
97
99
  // Merge doctor's --endpoint / --token flags into the env the resolver reads, so
@@ -10,6 +10,12 @@
10
10
  // remote → pass tool calls through to the hosted HTTP endpoint
11
11
  // off → advertise no tools; a call reports "disabled"
12
12
  //
13
+ // org.* tools are always advertised regardless of memory mode. They proxy to
14
+ // the remote endpoint because org management requires a server-side session
15
+ // (JWT auth, SECURITY DEFINER RPCs). In local/off mode a transient RemoteStore
16
+ // is built from the configured endpoint + token. If no remote is configured,
17
+ // a clear error is returned.
18
+ //
13
19
  // Machine-facing: ONLY JSON-RPC frames go to stdout — any diagnostics go to
14
20
  // stderr. The server never throws on malformed or partial input; a bad frame
15
21
  // yields a JSON-RPC parse error and the loop keeps serving.
@@ -19,6 +25,7 @@ import process from 'node:process';
19
25
  import { resolveProjectRoot } from './config.mjs';
20
26
  import { loadControl } from './control.mjs';
21
27
  import { createStore } from './store/index.mjs';
28
+ import { createRemoteStore } from './store/remote.mjs';
22
29
 
23
30
  const PROTOCOL_VERSION = '2024-11-05';
24
31
  const SERVER_INFO = { name: 'lorekit-local', version: '1.0.0' };
@@ -26,7 +33,7 @@ const SERVER_INFO = { name: 'lorekit-local', version: '1.0.0' };
26
33
  // Tool advertisements — names + input schemas mirror the production MCP server
27
34
  // (supabase/functions/mcp/mcp-handler.ts) so a client sees the same contract
28
35
  // whether it points at the hosted endpoint or this local server.
29
- export const TOOL_DEFS = [
36
+ export const MEMORY_TOOL_DEFS = [
30
37
  {
31
38
  name: 'memory.write',
32
39
  description: 'Store or update a lesson',
@@ -86,9 +93,62 @@ export const TOOL_DEFS = [
86
93
  },
87
94
  ];
88
95
 
96
+ // Org tools — always advertised regardless of memory mode. They always route
97
+ // through the remote MCP endpoint because org management requires JWT auth
98
+ // (SECURITY DEFINER RPCs resolve actor via auth.uid(), never a passed user_id).
99
+ export const ORG_TOOL_DEFS = [
100
+ {
101
+ name: 'org.create',
102
+ description:
103
+ 'Create a new organization. You become its owner automatically. ' +
104
+ 'The slug must be globally unique and lowercase (letters, digits, hyphens).',
105
+ inputSchema: {
106
+ type: 'object',
107
+ required: ['slug', 'name'],
108
+ properties: {
109
+ slug: { type: 'string', description: 'Unique lowercase identifier, e.g. "my-team"' },
110
+ name: { type: 'string', description: 'Human-readable display name' },
111
+ },
112
+ },
113
+ },
114
+ {
115
+ name: 'org.list',
116
+ description: 'List all organizations you are a member of, with your role in each.',
117
+ inputSchema: { type: 'object', properties: {} },
118
+ },
119
+ {
120
+ name: 'org.rename',
121
+ description: 'Rename an organization\'s display name. Requires admin or owner role.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ required: ['slug', 'name'],
125
+ properties: {
126
+ slug: { type: 'string', description: 'The org slug to update' },
127
+ name: { type: 'string', description: 'New display name' },
128
+ },
129
+ },
130
+ },
131
+ {
132
+ name: 'org.delete',
133
+ description:
134
+ 'Permanently delete an organization. Requires owner role. ' +
135
+ 'All org-owned memories and memberships are cascade-deleted. Unrecoverable.',
136
+ inputSchema: {
137
+ type: 'object',
138
+ required: ['slug'],
139
+ properties: {
140
+ slug: { type: 'string', description: 'The org slug to delete' },
141
+ },
142
+ },
143
+ },
144
+ ];
145
+
146
+ // Legacy alias kept so existing code that imports TOOL_DEFS still compiles.
147
+ export const TOOL_DEFS = [...MEMORY_TOOL_DEFS, ...ORG_TOOL_DEFS];
148
+
89
149
  // tool name → (store, args) → store result. The store destructures the args it
90
150
  // needs, so the raw `arguments` object is passed straight through.
91
- const DISPATCH = {
151
+ const MEMORY_DISPATCH = {
92
152
  'memory.write': (store, a) => store.write(a),
93
153
  'memory.read': (store, a) => store.read(a),
94
154
  'memory.list': (store, a) => store.list(a),
@@ -97,6 +157,14 @@ const DISPATCH = {
97
157
  'memory.archive': (store, a) => store.archive(a),
98
158
  };
99
159
 
160
+ // org.* dispatch — always routed to the remote store.
161
+ const ORG_DISPATCH = {
162
+ 'org.create': (remote, a) => remote.orgCreate(a),
163
+ 'org.list': (remote) => remote.orgList(),
164
+ 'org.rename': (remote, a) => remote.orgRename(a),
165
+ 'org.delete': (remote, a) => remote.orgDelete(a),
166
+ };
167
+
100
168
  function reply(id, result) {
101
169
  return { jsonrpc: '2.0', id, result };
102
170
  }
@@ -117,10 +185,22 @@ function toolResult(id, payload) {
117
185
  }
118
186
 
119
187
  // Build the per-message handler over a resolved control model. `store` is null
120
- // when mode is `off`, in which case no tools are advertised.
188
+ // when mode is `off`. Org tools are always advertised regardless of mode.
121
189
  export function createHandler(control) {
122
190
  const store = createStore(control);
123
- const tools = store ? TOOL_DEFS : [];
191
+ const memoryTools = store ? MEMORY_TOOL_DEFS : [];
192
+
193
+ // For org.* calls: prefer the active remote store (already built if mode is
194
+ // remote); fall back to building one from control.connection so org management
195
+ // works even in local/off modes as long as a remote endpoint is configured.
196
+ function getOrgRemote() {
197
+ if (store && store.mode === 'remote') return store;
198
+ const conn = (control && control.connection) || {};
199
+ if (conn.endpoint && conn.token) {
200
+ return createRemoteStore({ endpoint: conn.endpoint, token: conn.token });
201
+ }
202
+ return null;
203
+ }
124
204
 
125
205
  // Returns a JSON-RPC response object, or null for a notification (no reply).
126
206
  return async function handle(msg) {
@@ -142,7 +222,7 @@ export function createHandler(control) {
142
222
  }
143
223
 
144
224
  if (method === 'tools/list') {
145
- return reply(id, { tools });
225
+ return reply(id, { tools: [...memoryTools, ...ORG_TOOL_DEFS] });
146
226
  }
147
227
 
148
228
  if (method === 'tools/call') {
@@ -150,11 +230,29 @@ export function createHandler(control) {
150
230
  const name = params.name;
151
231
  const args = params.arguments || {};
152
232
 
233
+ // org.* tools — always proxy to remote.
234
+ if (name && name.startsWith('org.')) {
235
+ const fn = ORG_DISPATCH[name];
236
+ if (!fn) return errorReply(id, -32601, `Unknown tool: ${name}`);
237
+ const remote = getOrgRemote();
238
+ if (!remote) {
239
+ return toolResult(id, {
240
+ ok: false,
241
+ error:
242
+ 'org.* tools require a remote LoreKit endpoint configured with a read-write token. ' +
243
+ 'Set LOREKIT_MCP_URL and LOREKIT_TOKEN (lk_rw_*), or run `lorekit install --endpoint <url> --token <lk_rw_*>`.',
244
+ });
245
+ }
246
+ const result = await fn(remote, args);
247
+ return toolResult(id, result);
248
+ }
249
+
250
+ // memory.* tools
153
251
  if (!store) {
154
252
  return toolResult(id, { ok: false, error: `memory is disabled (mode: ${control.mode})` });
155
253
  }
156
254
 
157
- const fn = DISPATCH[name];
255
+ const fn = MEMORY_DISPATCH[name];
158
256
  if (!fn) return errorReply(id, -32601, `Unknown tool: ${name}`);
159
257
 
160
258
  const result = await fn(store, args);
@@ -82,6 +82,36 @@ class RemoteStore {
82
82
  return { ok: res.ok, error: res.error, networkError: res.networkError };
83
83
  }
84
84
 
85
+ // ── Org management ─────────────────────────────────────────────────────────
86
+ // These proxy to the hosted MCP endpoint's org.* tools. Auth is resolved
87
+ // server-side from the Bearer token; no user-id is passed by the caller.
88
+
89
+ async orgCreate({ slug, name } = {}) {
90
+ const res = await this._call('org.create', clean({ slug, name }));
91
+ if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
92
+ const payload = unwrap(res.result);
93
+ return { ok: true, org: payload };
94
+ }
95
+
96
+ async orgList() {
97
+ const res = await this._call('org.list', {});
98
+ return this._entries(res);
99
+ }
100
+
101
+ async orgRename({ slug, name } = {}) {
102
+ const res = await this._call('org.rename', clean({ slug, name }));
103
+ if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
104
+ const payload = unwrap(res.result);
105
+ return { ok: true, ...payload };
106
+ }
107
+
108
+ async orgDelete({ slug } = {}) {
109
+ const res = await this._call('org.delete', clean({ slug }));
110
+ if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
111
+ const payload = unwrap(res.result);
112
+ return { ok: true, ...payload };
113
+ }
114
+
85
115
  // Connectivity probe for doctor — a transport check, not a memory op.
86
116
  async ping() {
87
117
  if (!this.usable()) return { ok: false, unusable: true };
package/src/telemetry.mjs CHANGED
@@ -135,12 +135,13 @@ function resourceAttributes(version) {
135
135
  * Collect the bounded, non-PII attributes for a command invocation. Only the
136
136
  * command name, allow-listed boolean flags, the outcome and the exit code.
137
137
  */
138
- export function commandAttributes({ command, args = {}, outcome, exitCode }) {
138
+ export function commandAttributes({ command, args = {}, outcome, exitCode, extraAttrs = {} }) {
139
139
  const attrs = { 'lorekit.cli.command': command, 'lorekit.cli.outcome': outcome };
140
140
  if (typeof exitCode === 'number') attrs['lorekit.cli.exit_code'] = exitCode;
141
141
  for (const flag of FLAG_ATTRS) {
142
142
  if (args[flag]) attrs[`lorekit.cli.flag.${flag}`] = true;
143
143
  }
144
+ Object.assign(attrs, extraAttrs);
144
145
  return attrs;
145
146
  }
146
147
 
@@ -280,8 +281,23 @@ export async function traceCommand(command, args, version, run) {
280
281
  let exitCode = 0;
281
282
  let status = 'ok';
282
283
  let statusMessage;
284
+ let extraAttrs = {};
283
285
  try {
284
- exitCode = await run();
286
+ const result = await run();
287
+ // Commands may return either a plain exit code (number) or an object with
288
+ // { exitCode, ...extra } — the latter lets commands surface bounded,
289
+ // non-PII diagnostic fields (e.g. which checks failed in `doctor`).
290
+ if (result !== null && typeof result === 'object') {
291
+ exitCode = result.exitCode ?? 0;
292
+ const { exitCode: _ec, ...rest } = result;
293
+ // Flatten any array-valued extras to a comma-joined string so they fit
294
+ // the flat attribute bag shape (OTLP stringValue).
295
+ for (const [k, v] of Object.entries(rest)) {
296
+ extraAttrs[k] = Array.isArray(v) ? v.join(',') : v;
297
+ }
298
+ } else {
299
+ exitCode = result ?? 0;
300
+ }
285
301
  if (typeof exitCode === 'number' && exitCode !== 0) {
286
302
  status = 'error';
287
303
  statusMessage = `exit ${exitCode}`;
@@ -308,6 +324,7 @@ export async function traceCommand(command, args, version, run) {
308
324
  args,
309
325
  outcome: status === 'error' ? 'error' : 'ok',
310
326
  exitCode: typeof exitCode === 'number' ? exitCode : undefined,
327
+ extraAttrs,
311
328
  });
312
329
  await exportInvocation(config, {
313
330
  version,