@lorekit/cli 1.19.0 → 1.20.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
@@ -18,6 +18,7 @@ import { lint } from '../src/lint.mjs';
18
18
  import { dedupe } from '../src/dedupe.mjs';
19
19
  import { hook } from '../src/hook.mjs';
20
20
  import { migrate } from '../src/migrate.mjs';
21
+ import { bootstrap } from '../src/bootstrap.mjs';
21
22
  import { mcpServer } from '../src/mcp-server.mjs';
22
23
  import { traceCommand } from '../src/telemetry.mjs';
23
24
  import { loadDotEnv } from '../src/dotenv.mjs';
@@ -80,6 +81,9 @@ ${c.bold('Commands')}
80
81
  dedupe Find likely-duplicate memories via a zero-dep word-overlap HEURISTIC
81
82
  (Jaccard >= threshold, not semantic), grouped into clusters per
82
83
  store. --json, --scope <s>, --threshold <0..1>.
84
+ bootstrap Apply the BYOD schema to a user-supplied Supabase database.
85
+ Only needed when using LOREKIT_STORAGE_URL / LOREKIT_STORAGE_ANON_KEY.
86
+ See docs/byod.md for setup instructions.
83
87
  migrate Relocate a LoreKit-format local store into the current layout.
84
88
  Dry-run by default; pass --yes to apply. Idempotent.
85
89
  hook Hook engine for Claude Code / Cursor / Codex. Reads the host's
@@ -613,6 +617,8 @@ async function main() {
613
617
  return traceCommand('dedupe', args, VERSION, () => dedupe(args));
614
618
  case 'migrate':
615
619
  return traceCommand('migrate', args, VERSION, () => migrate(args));
620
+ case 'bootstrap':
621
+ return traceCommand('bootstrap', args, VERSION, () => bootstrap(args));
616
622
  case 'write':
617
623
  return traceCommand('write', args, VERSION, () => write(args));
618
624
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.19.0",
3
+ "version": "1.20.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": {
@@ -0,0 +1,141 @@
1
+ // `lorekit bootstrap` — apply the BYOD schema to a user-supplied Supabase database.
2
+ //
3
+ // Reads LOREKIT_STORAGE_URL and LOREKIT_STORAGE_SERVICE_KEY from the environment.
4
+ // If neither is set, prints a helpful message and exits 0 — bootstrap is only
5
+ // needed for BYOD (Bring Your Own Database) setups.
6
+ //
7
+ // Because Supabase's JS client does not expose a raw SQL execution method for DDL,
8
+ // this command instructs the user to run the SQL file directly with psql when no
9
+ // direct execution path is available. It validates connectivity using the anon key
10
+ // and confirms the bootstrap.sql path for the user.
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import process from 'node:process';
15
+ import { log, err, heading, status, c } from './util.mjs';
16
+
17
+ // The bootstrap.sql file is at <repo-root>/supabase/byod/bootstrap.sql.
18
+ // This file lives at packages/cli/src/bootstrap.mjs, so the relative path
19
+ // from here to the repo root is ../../../ (src → cli → packages → root).
20
+ const BOOTSTRAP_SQL_PATH = fileURLToPath(
21
+ new URL('../../../supabase/byod/bootstrap.sql', import.meta.url),
22
+ );
23
+
24
+ export async function bootstrap(_args) {
25
+ heading('LoreKit bootstrap');
26
+
27
+ const storageUrl = process.env['LOREKIT_STORAGE_URL'];
28
+ const storageServiceKey = process.env['LOREKIT_STORAGE_SERVICE_KEY'];
29
+ const storageAnonKey = process.env['LOREKIT_STORAGE_ANON_KEY'];
30
+
31
+ // If no BYOD env vars are set, this command is a no-op — it's only for BYOD.
32
+ if (!storageUrl && !storageServiceKey) {
33
+ log('');
34
+ log(
35
+ ` ${c.cyan('•')} No BYOD storage configured — ${c.dim('bootstrap is only needed for custom databases.')}`,
36
+ );
37
+ log('');
38
+ log(' Set LOREKIT_STORAGE_URL and LOREKIT_STORAGE_SERVICE_KEY to use your own');
39
+ log(' Supabase project, then re-run this command.');
40
+ log('');
41
+ log(' See docs/byod.md for setup instructions.');
42
+ return 0;
43
+ }
44
+
45
+ if (!storageUrl) {
46
+ err(`${c.red('Error:')} LOREKIT_STORAGE_SERVICE_KEY is set but LOREKIT_STORAGE_URL is missing.`);
47
+ err(' Both variables are required. Set LOREKIT_STORAGE_URL and try again.');
48
+ return 1;
49
+ }
50
+
51
+ // Locate the bootstrap SQL file.
52
+ let sqlPath = BOOTSTRAP_SQL_PATH;
53
+
54
+ // When running from a published npm package, the SQL file is not bundled
55
+ // with the CLI. Fall back to looking for it relative to CWD (for dev use).
56
+ if (!fs.existsSync(sqlPath)) {
57
+ const cwdPath = path.resolve(process.cwd(), 'supabase/byod/bootstrap.sql');
58
+ if (fs.existsSync(cwdPath)) {
59
+ sqlPath = cwdPath;
60
+ } else {
61
+ err(`${c.red('Error:')} bootstrap.sql not found at expected path:`);
62
+ err(` ${sqlPath}`);
63
+ err('');
64
+ err(' Apply the schema manually with psql:');
65
+ err(' psql "$DATABASE_URL" -f supabase/byod/bootstrap.sql');
66
+ err('');
67
+ err(' Or download it from the LoreKit repository:');
68
+ err(' https://github.com/mthines/lorekit/blob/main/supabase/byod/bootstrap.sql');
69
+ return 1;
70
+ }
71
+ }
72
+
73
+ status('pass', 'bootstrap.sql', sqlPath);
74
+ status('info', 'storage url', storageUrl);
75
+
76
+ // Test connectivity using the anon key if available (non-DDL path).
77
+ if (storageAnonKey) {
78
+ try {
79
+ const { createClient } = await import('@supabase/supabase-js');
80
+ const db = createClient(storageUrl, storageAnonKey, {
81
+ auth: { persistSession: false, autoRefreshToken: false },
82
+ });
83
+ // Simple ping: list memories (will return 0 rows or error if schema not applied yet).
84
+ const { error } = await db.from('memories').select('id').limit(1);
85
+ if (error && error.code === '42P01') {
86
+ // Table does not exist yet — that's expected before bootstrap.
87
+ status('info', 'connectivity', 'connected — schema not yet applied (run bootstrap)');
88
+ } else if (error) {
89
+ status('warn', 'connectivity', `connected but got: ${error.message}`);
90
+ } else {
91
+ status('pass', 'connectivity', 'connected and schema already present');
92
+ }
93
+ } catch (e) {
94
+ status('warn', 'connectivity', `could not verify: ${e && e.message ? e.message : String(e)}`);
95
+ }
96
+ } else {
97
+ status('info', 'connectivity', 'skipped — LOREKIT_STORAGE_ANON_KEY not set');
98
+ }
99
+
100
+ // Supabase JS client cannot execute raw DDL SQL directly.
101
+ // The correct path is psql or the Supabase dashboard SQL editor.
102
+ log('');
103
+ log(` ${c.bold('To apply the schema, run:')}`);
104
+ log('');
105
+ log(` ${c.cyan('psql "$DATABASE_URL" -f')} ${sqlPath}`);
106
+ log('');
107
+ log(` Or paste the contents of ${c.dim(sqlPath)}`);
108
+ log(' into the Supabase dashboard → SQL Editor.');
109
+ log('');
110
+
111
+ if (storageServiceKey) {
112
+ status(
113
+ 'info',
114
+ 'service key',
115
+ 'LOREKIT_STORAGE_SERVICE_KEY is set — use it as $DATABASE_URL password with psql',
116
+ );
117
+ log('');
118
+ log(` ${c.dim('Example:')}`);
119
+ log(
120
+ ` ${c.cyan('DATABASE_URL')}="postgresql://postgres.${parseRef(storageUrl)}:${storageServiceKey}@aws-0-us-east-1.pooler.supabase.com:6543/postgres"`,
121
+ );
122
+ log(` ${c.cyan('psql "$DATABASE_URL"')} -f ${sqlPath}`);
123
+ log('');
124
+ log(
125
+ ` ${c.dim('(Replace the host/port above with the exact connection string from your')}`,
126
+ );
127
+ log(` ${c.dim('Supabase dashboard → Settings → Database → Connection string.)')}`);
128
+ }
129
+
130
+ return 0;
131
+ }
132
+
133
+ // Extract the project ref from a Supabase URL for display in the example.
134
+ function parseRef(url) {
135
+ try {
136
+ const host = new URL(url).hostname; // e.g. abcdefgh.supabase.co
137
+ return host.split('.')[0];
138
+ } catch {
139
+ return '<project-ref>';
140
+ }
141
+ }
package/src/doctor.mjs CHANGED
@@ -98,6 +98,9 @@ export async function doctor(args) {
98
98
  await checkRemote(control, root, args, record);
99
99
  }
100
100
 
101
+ // 4b. BYOD storage connectivity check.
102
+ await checkBYODStorage(record);
103
+
101
104
  // 5. Scope.
102
105
  const scope = deriveScope(root);
103
106
  if (scope.hasRemote) {
@@ -351,6 +354,40 @@ function hooksForEvent(hooksObj, event) {
351
354
  return commands;
352
355
  }
353
356
 
357
+ async function checkBYODStorage(record) {
358
+ const storageUrl = process.env['LOREKIT_STORAGE_URL'];
359
+ const storageAnonKey = process.env['LOREKIT_STORAGE_ANON_KEY'];
360
+
361
+ if (!storageUrl && !storageAnonKey) {
362
+ return; // No BYOD configured — skip silently.
363
+ }
364
+
365
+ if (storageUrl && !storageAnonKey) {
366
+ record('fail', 'byod storage', 'LOREKIT_STORAGE_URL is set but LOREKIT_STORAGE_ANON_KEY is missing');
367
+ return;
368
+ }
369
+
370
+ if (!storageUrl && storageAnonKey) {
371
+ record('fail', 'byod storage', 'LOREKIT_STORAGE_ANON_KEY is set but LOREKIT_STORAGE_URL is missing');
372
+ return;
373
+ }
374
+
375
+ try {
376
+ const { createClient } = await import('@supabase/supabase-js');
377
+ const db = createClient(storageUrl, storageAnonKey, {
378
+ auth: { persistSession: false, autoRefreshToken: false },
379
+ });
380
+ const { error } = await db.from('memories').select('id').limit(1);
381
+ if (error && error.code !== '42P01') {
382
+ record('fail', 'byod storage', `connectivity error: ${error.message}`);
383
+ } else {
384
+ record('pass', 'byod storage', `ok — ${storageUrl}`);
385
+ }
386
+ } catch (e) {
387
+ record('fail', 'byod storage', `could not connect: ${e && e.message ? e.message : String(e)}`);
388
+ }
389
+ }
390
+
354
391
  function gitTracked(root, dir) {
355
392
  // Heuristic: is the store dir ignored by git? If `git check-ignore` names it,
356
393
  // it is private; otherwise it will be committed (team-shared).