superlore-cli 0.1.2 → 0.3.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 +7 -1
- package/dist/config.d.ts +7 -3
- package/dist/config.js +5 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +732 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,11 +40,17 @@ curl -fsSL https://superlore.vercel.app/install.sh | sh # or: npm i -g superl
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
superlore init my-kb # scaffold — 2 questions → superlore.json + starter pages
|
|
43
|
+
superlore init my-kb # scaffold — 2 questions → superlore.json + starter pages, then sets up your editor
|
|
44
|
+
superlore connect # detect VS Code / Cursor / Windsurf and install the live-preview extension
|
|
44
45
|
superlore dev # live preview at localhost:3000
|
|
45
46
|
superlore build # production build, deploy anywhere
|
|
46
47
|
```
|
|
47
48
|
|
|
49
|
+
`init` ends by offering to run `connect` for you, so one command takes you from nothing to a
|
|
50
|
+
scaffolded KB with the editor extension installed — then it points you at wiring the MCP to your
|
|
51
|
+
agent. `connect` detects each editor via its CLI or standard install path (macOS / Linux / Windows)
|
|
52
|
+
and is safe to re-run.
|
|
53
|
+
|
|
48
54
|
`superlore deploy` is reserved for managed **superlore Cloud**
|
|
49
55
|
([waitlisted](https://superlore.vercel.app/cloud)) — self-host free with `superlore build`.
|
|
50
56
|
|
package/dist/config.d.ts
CHANGED
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
* imported from anywhere — including the browser or an edge runtime — without dragging in a
|
|
12
12
|
* validator. Keep it that way.
|
|
13
13
|
*/
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The kinds of superlore KB. Drives defaults — the auth warning for company KBs, and an
|
|
16
|
+
* auth-ON-by-default, MCP-ON private posture for a `personal-kb` (a digital replica of how one
|
|
17
|
+
* person thinks, works, and writes).
|
|
18
|
+
*/
|
|
19
|
+
type SuperloreType = "company-kb" | "product-docs" | "personal-kb";
|
|
16
20
|
/** Supported SSO providers. Only Google ships today (Auth.js v5 + Google SSO). */
|
|
17
21
|
type SuperloreAuthProvider = "google";
|
|
18
22
|
/** The human gate. Optional and per-deploy — public by default. */
|
|
@@ -35,7 +39,7 @@ interface SuperloreMcpConfig {
|
|
|
35
39
|
interface SuperloreJson {
|
|
36
40
|
/** Human-facing KB name. */
|
|
37
41
|
name: string;
|
|
38
|
-
/** Whether this is a private company KB
|
|
42
|
+
/** Whether this is a private company KB, public product docs, or a private personal KB. */
|
|
39
43
|
type: SuperloreType;
|
|
40
44
|
/** Brand accent — any CSS colour. superlore derives the rest of the family (light + dark). */
|
|
41
45
|
accent?: string;
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
var DEFAULT_MCP_PATH = "/api/mcp";
|
|
3
3
|
var SUPERLORE_JSON_FILENAME = "superlore.json";
|
|
4
|
-
var SUPERLORE_TYPES = [
|
|
4
|
+
var SUPERLORE_TYPES = [
|
|
5
|
+
"company-kb",
|
|
6
|
+
"product-docs",
|
|
7
|
+
"personal-kb"
|
|
8
|
+
];
|
|
5
9
|
function isRecord(value) {
|
|
6
10
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
11
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,8 +13,12 @@ import * as cac from 'cac';
|
|
|
13
13
|
* imported from anywhere — including the browser or an edge runtime — without dragging in a
|
|
14
14
|
* validator. Keep it that way.
|
|
15
15
|
*/
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The kinds of superlore KB. Drives defaults — the auth warning for company KBs, and an
|
|
18
|
+
* auth-ON-by-default, MCP-ON private posture for a `personal-kb` (a digital replica of how one
|
|
19
|
+
* person thinks, works, and writes).
|
|
20
|
+
*/
|
|
21
|
+
type SuperloreType = "company-kb" | "product-docs" | "personal-kb";
|
|
18
22
|
/** Supported SSO providers. Only Google ships today (Auth.js v5 + Google SSO). */
|
|
19
23
|
type SuperloreAuthProvider = "google";
|
|
20
24
|
/** The human gate. Optional and per-deploy — public by default. */
|
|
@@ -37,7 +41,7 @@ interface SuperloreMcpConfig {
|
|
|
37
41
|
interface SuperloreJson {
|
|
38
42
|
/** Human-facing KB name. */
|
|
39
43
|
name: string;
|
|
40
|
-
/** Whether this is a private company KB
|
|
44
|
+
/** Whether this is a private company KB, public product docs, or a private personal KB. */
|
|
41
45
|
type: SuperloreType;
|
|
42
46
|
/** Brand accent — any CSS colour. superlore derives the rest of the family (light + dark). */
|
|
43
47
|
accent?: string;
|
|
@@ -83,7 +87,7 @@ declare function serializeSuperloreJson(config: SuperloreJson): string;
|
|
|
83
87
|
declare function resolveMcpPath(config: SuperloreJson): string | undefined;
|
|
84
88
|
|
|
85
89
|
/** The CLI version, kept in sync with package.json at build time. */
|
|
86
|
-
declare const VERSION = "0.
|
|
90
|
+
declare const VERSION = "0.3.0";
|
|
87
91
|
/** Build the argument parser. Exported for tests; `run()` wires it to argv. */
|
|
88
92
|
declare function buildCli(argv?: readonly string[]): cac.CAC;
|
|
89
93
|
/** Parse argv and dispatch. Reports unknown commands and unexpected errors cleanly. */
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5
|
-
import
|
|
5
|
+
import process3 from "process";
|
|
6
6
|
import { cac } from "cac";
|
|
7
7
|
|
|
8
8
|
// src/lib/log.ts
|
|
@@ -71,7 +71,11 @@ import { dirname, join, resolve } from "path";
|
|
|
71
71
|
// src/config.ts
|
|
72
72
|
var DEFAULT_MCP_PATH = "/api/mcp";
|
|
73
73
|
var SUPERLORE_JSON_FILENAME = "superlore.json";
|
|
74
|
-
var SUPERLORE_TYPES = [
|
|
74
|
+
var SUPERLORE_TYPES = [
|
|
75
|
+
"company-kb",
|
|
76
|
+
"product-docs",
|
|
77
|
+
"personal-kb"
|
|
78
|
+
];
|
|
75
79
|
function isRecord(value) {
|
|
76
80
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
77
81
|
}
|
|
@@ -253,6 +257,194 @@ async function buildCommand() {
|
|
|
253
257
|
process.exit(code);
|
|
254
258
|
}
|
|
255
259
|
|
|
260
|
+
// src/lib/editors.ts
|
|
261
|
+
import { execFileSync } from "child_process";
|
|
262
|
+
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
263
|
+
import { homedir, tmpdir } from "os";
|
|
264
|
+
import { join as join2 } from "path";
|
|
265
|
+
import process2 from "process";
|
|
266
|
+
var EXTENSION_ID = "superlore.superlore-preview";
|
|
267
|
+
var DEFAULT_VSIX_URL = "https://superlore.vercel.app/superlore-preview.vsix";
|
|
268
|
+
async function downloadVsix(url = DEFAULT_VSIX_URL) {
|
|
269
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
270
|
+
if (!res.ok) throw new Error(`couldn't fetch the extension (${res.status} ${res.statusText})`);
|
|
271
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
272
|
+
const path = join2(tmpdir(), "superlore-preview.vsix");
|
|
273
|
+
writeFileSync(path, bytes);
|
|
274
|
+
return path;
|
|
275
|
+
}
|
|
276
|
+
var EDITORS = [
|
|
277
|
+
{ id: "vscode", label: "VS Code", bin: "code" },
|
|
278
|
+
{ id: "cursor", label: "Cursor", bin: "cursor" },
|
|
279
|
+
{ id: "windsurf", label: "Windsurf", bin: "windsurf" }
|
|
280
|
+
];
|
|
281
|
+
function onPath(bin) {
|
|
282
|
+
const probe = process2.platform === "win32" ? "where" : "which";
|
|
283
|
+
try {
|
|
284
|
+
execFileSync(probe, [bin], { stdio: "ignore", shell: process2.platform === "win32" });
|
|
285
|
+
return true;
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function fallbackPaths(editor) {
|
|
291
|
+
const home = homedir();
|
|
292
|
+
if (process2.platform === "darwin") {
|
|
293
|
+
const app = {
|
|
294
|
+
vscode: "Visual Studio Code.app",
|
|
295
|
+
cursor: "Cursor.app",
|
|
296
|
+
windsurf: "Windsurf.app"
|
|
297
|
+
};
|
|
298
|
+
const rel = `Contents/Resources/app/bin/${editor.bin}`;
|
|
299
|
+
return [
|
|
300
|
+
join2("/Applications", app[editor.id], rel),
|
|
301
|
+
join2(home, "Applications", app[editor.id], rel)
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
if (process2.platform === "win32") {
|
|
305
|
+
const local = process2.env.LOCALAPPDATA ?? join2(home, "AppData", "Local");
|
|
306
|
+
const programs = join2(local, "Programs");
|
|
307
|
+
const dir = {
|
|
308
|
+
vscode: "Microsoft VS Code",
|
|
309
|
+
cursor: "cursor",
|
|
310
|
+
windsurf: "Windsurf"
|
|
311
|
+
};
|
|
312
|
+
const exe = `bin\\${editor.bin}.cmd`;
|
|
313
|
+
return [
|
|
314
|
+
join2(programs, dir[editor.id], exe),
|
|
315
|
+
join2(process2.env.ProgramFiles ?? "C:\\Program Files", dir[editor.id], exe)
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
return [
|
|
319
|
+
join2("/usr/share", editor.bin, "bin", editor.bin),
|
|
320
|
+
join2("/usr/bin", editor.bin),
|
|
321
|
+
join2("/snap/bin", editor.bin),
|
|
322
|
+
join2(home, ".local", "bin", editor.bin)
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
function resolveEditorCommand(editor) {
|
|
326
|
+
if (onPath(editor.bin)) return editor.bin;
|
|
327
|
+
for (const candidate of fallbackPaths(editor)) {
|
|
328
|
+
if (existsSync2(candidate)) return candidate;
|
|
329
|
+
}
|
|
330
|
+
return void 0;
|
|
331
|
+
}
|
|
332
|
+
function detectEditors() {
|
|
333
|
+
const found = [];
|
|
334
|
+
for (const editor of EDITORS) {
|
|
335
|
+
const command = resolveEditorCommand(editor);
|
|
336
|
+
if (command) found.push({ ...editor, command });
|
|
337
|
+
}
|
|
338
|
+
return found;
|
|
339
|
+
}
|
|
340
|
+
var execRunner = (command, args) => execFileSync(command, [...args], {
|
|
341
|
+
encoding: "utf8",
|
|
342
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
343
|
+
// Editor CLIs on Windows are .cmd shims; a shell is needed to invoke them.
|
|
344
|
+
shell: process2.platform === "win32",
|
|
345
|
+
timeout: 6e4
|
|
346
|
+
}).trim();
|
|
347
|
+
function classifyInstallOutput(editor, output) {
|
|
348
|
+
return /already installed/i.test(output) ? { editor, status: "already-installed" } : { editor, status: "installed" };
|
|
349
|
+
}
|
|
350
|
+
function installInto(editor, options = {}) {
|
|
351
|
+
const run2 = options.run ?? execRunner;
|
|
352
|
+
const target = options.vsix ?? EXTENSION_ID;
|
|
353
|
+
try {
|
|
354
|
+
const output = run2(editor.command, ["--install-extension", target, "--force"]);
|
|
355
|
+
return classifyInstallOutput(editor, output);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return { editor, status: "failed", error: failureReason(error) };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function failureReason(error) {
|
|
361
|
+
if (error instanceof Error) {
|
|
362
|
+
const stderr = error.stderr;
|
|
363
|
+
const text2 = typeof stderr === "string" ? stderr : Buffer.isBuffer(stderr) ? stderr.toString() : "";
|
|
364
|
+
const lines = text2.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !/^\(node:/.test(l) && !/^\(Use /.test(l));
|
|
365
|
+
const meaningful = lines.find((l) => /(not found|error|fail|denied)/i.test(l)) ?? lines[0];
|
|
366
|
+
if (meaningful) return meaningful;
|
|
367
|
+
return error.message.replace(/^Command failed:.*/s, "the editor CLI returned an error").trim();
|
|
368
|
+
}
|
|
369
|
+
return String(error);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/commands/connect.ts
|
|
373
|
+
async function connectCommand(flags = {}) {
|
|
374
|
+
log.blank();
|
|
375
|
+
log.info(`${accent("superlore connect")} ${dim("\u2014 set up your editor")}`);
|
|
376
|
+
log.blank();
|
|
377
|
+
const detected = detectEditors();
|
|
378
|
+
if (detected.length === 0) {
|
|
379
|
+
log.warn("No supported editor detected (looked for VS Code, Cursor, and Windsurf).");
|
|
380
|
+
log.blank();
|
|
381
|
+
log.info(`${dim("Install one, then re-run")} ${cyan("superlore connect")}${dim(".")}`);
|
|
382
|
+
log.info(
|
|
383
|
+
`${dim("If an editor is installed but its CLI isn't on PATH, open it and run")} ${cyan(`"Shell Command: Install '<editor>' command in PATH"`)}${dim(".")}`
|
|
384
|
+
);
|
|
385
|
+
printMcpNextStep();
|
|
386
|
+
process.exit(0);
|
|
387
|
+
}
|
|
388
|
+
const labels = detected.map((e) => bold(e.label)).join(", ");
|
|
389
|
+
log.step(`Found ${labels}. Installing the superlore Preview extension\u2026`);
|
|
390
|
+
log.blank();
|
|
391
|
+
let vsix;
|
|
392
|
+
try {
|
|
393
|
+
vsix = flags.vsix ?? await downloadVsix();
|
|
394
|
+
} catch (error) {
|
|
395
|
+
log.error(
|
|
396
|
+
`Couldn't fetch the extension: ${error instanceof Error ? error.message : String(error)}`
|
|
397
|
+
);
|
|
398
|
+
printManualInstall();
|
|
399
|
+
process.exit(flags.optional ? 0 : 1);
|
|
400
|
+
}
|
|
401
|
+
const results = detected.map((editor) => report(installInto(editor, { vsix })));
|
|
402
|
+
log.blank();
|
|
403
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
404
|
+
if (failed.length > 0 && failed.length === results.length) {
|
|
405
|
+
log.error("Couldn't install the extension into any editor. See the errors above.");
|
|
406
|
+
printManualInstall();
|
|
407
|
+
process.exit(flags.optional ? 0 : 1);
|
|
408
|
+
}
|
|
409
|
+
log.success(
|
|
410
|
+
`superlore Preview is ready. Open a ${cyan(".mdx")} file and run ${cyan("superlore: Open Preview")} ${dim("(Cmd/Ctrl+K V)")}.`
|
|
411
|
+
);
|
|
412
|
+
printMcpNextStep();
|
|
413
|
+
}
|
|
414
|
+
function report(result) {
|
|
415
|
+
switch (result.status) {
|
|
416
|
+
case "installed":
|
|
417
|
+
log.success(`${bold(result.editor.label)} ${dim("\u2014 extension installed.")}`);
|
|
418
|
+
break;
|
|
419
|
+
case "already-installed":
|
|
420
|
+
log.info(`${accent("\u203A")} ${bold(result.editor.label)} ${dim("\u2014 already installed, up to date.")}`);
|
|
421
|
+
break;
|
|
422
|
+
case "failed":
|
|
423
|
+
log.error(`${bold(result.editor.label)} ${dim("\u2014 install failed:")} ${result.error}`);
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
function printManualInstall() {
|
|
429
|
+
log.blank();
|
|
430
|
+
log.info(
|
|
431
|
+
`${dim("Install it by hand: download")} ${cyan("superlore.vercel.app/superlore-preview.vsix")}${dim(",")}`
|
|
432
|
+
);
|
|
433
|
+
log.info(
|
|
434
|
+
`${dim("then run")} ${cyan('"Extensions: Install from VSIX\u2026"')} ${dim("in your editor (or")} ${cyan("code --install-extension <file>.vsix")}${dim(").")}`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
function printMcpNextStep() {
|
|
438
|
+
log.blank();
|
|
439
|
+
log.info(bold("Next: connect the MCP"));
|
|
440
|
+
log.info(
|
|
441
|
+
` ${dim("Let your agent read the same corpus. Ask Claude")} ${cyan('"connect my superlore MCP"')}${dim(",")}`
|
|
442
|
+
);
|
|
443
|
+
log.info(
|
|
444
|
+
` ${dim("or register it yourself:")} ${cyan("claude mcp add --transport http -s user superlore <url>/api/mcp")}`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
256
448
|
// src/commands/deploy.ts
|
|
257
449
|
import { spawn as spawn2 } from "child_process";
|
|
258
450
|
|
|
@@ -332,34 +524,34 @@ async function devCommand(flags) {
|
|
|
332
524
|
}
|
|
333
525
|
|
|
334
526
|
// src/commands/init.ts
|
|
335
|
-
import { existsSync as
|
|
527
|
+
import { existsSync as existsSync4 } from "fs";
|
|
336
528
|
import { basename, resolve as resolve3 } from "path";
|
|
337
529
|
import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
|
|
338
530
|
|
|
339
531
|
// src/lib/scaffold.ts
|
|
340
|
-
import { cpSync, existsSync as
|
|
532
|
+
import { cpSync, existsSync as existsSync3, mkdirSync, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
341
533
|
import { fileURLToPath } from "url";
|
|
342
|
-
import { dirname as dirname2, join as
|
|
534
|
+
import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
|
|
343
535
|
var here = dirname2(fileURLToPath(import.meta.url));
|
|
344
536
|
function findStarterTemplate() {
|
|
345
537
|
let dir = here;
|
|
346
538
|
for (; ; ) {
|
|
347
|
-
const candidate =
|
|
348
|
-
if (
|
|
349
|
-
const bundled =
|
|
350
|
-
if (
|
|
539
|
+
const candidate = join3(dir, "templates", "starter");
|
|
540
|
+
if (existsSync3(candidate)) return candidate;
|
|
541
|
+
const bundled = join3(dir, "template");
|
|
542
|
+
if (existsSync3(bundled)) return bundled;
|
|
351
543
|
const parent = dirname2(dir);
|
|
352
544
|
if (parent === dir) return void 0;
|
|
353
545
|
dir = parent;
|
|
354
546
|
}
|
|
355
547
|
}
|
|
356
548
|
function isUsableTemplate(dir) {
|
|
357
|
-
if (!
|
|
549
|
+
if (!existsSync3(dir)) return false;
|
|
358
550
|
const entries = readdirSync(dir).filter((name) => name !== "README.md" && !name.startsWith("."));
|
|
359
551
|
return entries.length > 0;
|
|
360
552
|
}
|
|
361
553
|
function isEmptyDir(dir) {
|
|
362
|
-
if (!
|
|
554
|
+
if (!existsSync3(dir)) return true;
|
|
363
555
|
return readdirSync(dir).filter((n) => !n.startsWith(".")).length === 0;
|
|
364
556
|
}
|
|
365
557
|
function scaffold(options) {
|
|
@@ -374,17 +566,18 @@ function scaffold(options) {
|
|
|
374
566
|
writeSkeleton(root, options.config);
|
|
375
567
|
source = "skeleton";
|
|
376
568
|
}
|
|
377
|
-
|
|
569
|
+
writeFileSync2(join3(root, "superlore.json"), serializeSuperloreJson(options.config), "utf8");
|
|
378
570
|
return { root, source };
|
|
379
571
|
}
|
|
380
572
|
function writeSkeleton(root, config) {
|
|
381
573
|
const accent2 = config.accent ?? SUPERLORE_VIOLET;
|
|
382
574
|
const mcpEnabled = config.mcp?.enabled ?? true;
|
|
575
|
+
const authEnabled = config.auth?.enabled ?? false;
|
|
383
576
|
const slug = config.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "superlore-kb";
|
|
384
577
|
const write = (rel, body) => {
|
|
385
|
-
const file =
|
|
578
|
+
const file = join3(root, rel);
|
|
386
579
|
mkdirSync(dirname2(file), { recursive: true });
|
|
387
|
-
|
|
580
|
+
writeFileSync2(file, body, "utf8");
|
|
388
581
|
};
|
|
389
582
|
write(
|
|
390
583
|
"package.json",
|
|
@@ -404,9 +597,14 @@ function writeSkeleton(root, config) {
|
|
|
404
597
|
"fumadocs-core": "16.8.2",
|
|
405
598
|
"fumadocs-mdx": "14.3.1",
|
|
406
599
|
"fumadocs-ui": "16.8.2",
|
|
407
|
-
superlore: "^0.1
|
|
600
|
+
superlore: "^0.5.1",
|
|
408
601
|
"lucide-react": "^1.21.0",
|
|
602
|
+
// superlore peers the rendered components pull in: Mermaid (Diagram), themes.
|
|
603
|
+
mermaid: "^11.15.0",
|
|
409
604
|
...mcpEnabled ? { "@modelcontextprotocol/sdk": "^1.29.0", "mcp-handler": "^1.1.0" } : {},
|
|
605
|
+
// Auth.js v5 powers the optional Google SSO gate (superlore/auth). Self-disabling
|
|
606
|
+
// without AUTH_GOOGLE_ID, so it's harmless until the env is set.
|
|
607
|
+
...authEnabled ? { "next-auth": "^5.0.0-beta.25" } : {},
|
|
410
608
|
next: "16.2.4",
|
|
411
609
|
"next-themes": "^0.4.6",
|
|
412
610
|
react: "^19.2.5",
|
|
@@ -445,7 +643,7 @@ function writeSkeleton(root, config) {
|
|
|
445
643
|
isolatedModules: true,
|
|
446
644
|
skipLibCheck: true,
|
|
447
645
|
incremental: true,
|
|
448
|
-
paths: { "@/*": ["./*"] },
|
|
646
|
+
paths: { "@/*": ["./*"], "collections/*": ["./.source/*"] },
|
|
449
647
|
plugins: [{ name: "next" }]
|
|
450
648
|
},
|
|
451
649
|
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
@@ -465,8 +663,7 @@ const withMDX = createMDX();
|
|
|
465
663
|
/** @type {import('next').NextConfig} */
|
|
466
664
|
const config = {
|
|
467
665
|
reactStrictMode: true,
|
|
468
|
-
//
|
|
469
|
-
transpilePackages: ["superlore"],
|
|
666
|
+
// superlore ships compiled ESM \u2014 consume it as a normal package (no transpilePackages).
|
|
470
667
|
};
|
|
471
668
|
|
|
472
669
|
export default withMDX(config);
|
|
@@ -510,7 +707,7 @@ export default config;
|
|
|
510
707
|
write(
|
|
511
708
|
"app/layout.tsx",
|
|
512
709
|
`import type { ReactNode } from "react";
|
|
513
|
-
import { RootProvider } from "
|
|
710
|
+
import { RootProvider } from "superlore/ui";
|
|
514
711
|
import "./global.css";
|
|
515
712
|
|
|
516
713
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
@@ -584,18 +781,22 @@ export function generateStaticParams() {
|
|
|
584
781
|
);
|
|
585
782
|
write(
|
|
586
783
|
"lib/source.ts",
|
|
587
|
-
`import {
|
|
588
|
-
import {
|
|
784
|
+
`import { docs } from "collections/server";
|
|
785
|
+
import { loader, lucideIconsPlugin } from "superlore/source";
|
|
589
786
|
|
|
590
787
|
export const source = loader({
|
|
591
788
|
baseUrl: "/docs",
|
|
592
789
|
source: docs.toFumadocsSource(),
|
|
790
|
+
plugins: [lucideIconsPlugin()],
|
|
593
791
|
});
|
|
594
792
|
`
|
|
595
793
|
);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
794
|
+
if (config.type === "personal-kb") {
|
|
795
|
+
writePersonalContent(write, config);
|
|
796
|
+
} else {
|
|
797
|
+
write(
|
|
798
|
+
"content/docs/index.mdx",
|
|
799
|
+
`---
|
|
599
800
|
title: Welcome
|
|
600
801
|
description: The home of your superlore knowledge base.
|
|
601
802
|
summary: Landing page for the ${config.name} knowledge base.
|
|
@@ -609,7 +810,11 @@ agents read the same structured content over MCP.
|
|
|
609
810
|
|
|
610
811
|
Edit \`content/docs/index.mdx\` to make it yours, then run \`superlore dev\`.
|
|
611
812
|
`
|
|
612
|
-
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (authEnabled) {
|
|
816
|
+
writeAuth(write, config);
|
|
817
|
+
}
|
|
613
818
|
if (mcpEnabled) {
|
|
614
819
|
const mcpPath = config.mcp?.path ?? "/api/mcp";
|
|
615
820
|
write(
|
|
@@ -617,10 +822,14 @@ Edit \`content/docs/index.mdx\` to make it yours, then run \`superlore dev\`.
|
|
|
617
822
|
`import { createMcpHandler } from "mcp-handler";
|
|
618
823
|
import { z } from "zod";
|
|
619
824
|
import { getComponentData, getPage, list, navigate, search } from "superlore/mcp";
|
|
825
|
+
import { buildIndexFromSource } from "superlore/source";
|
|
826
|
+
import type { KKind } from "superlore";
|
|
827
|
+
import { source } from "@/lib/source";
|
|
620
828
|
|
|
621
829
|
// Your KB's MCP endpoint. Served at ${mcpPath} \u2014 the same structured content the site renders,
|
|
622
|
-
// exposed to agents.
|
|
623
|
-
//
|
|
830
|
+
// exposed to agents. The index is built straight from your content \`source\`: author once, and
|
|
831
|
+
// humans read the pages while agents query this corpus. No scraping, no drift.
|
|
832
|
+
const index = buildIndexFromSource(source);
|
|
624
833
|
|
|
625
834
|
const json = (data: unknown) => ({
|
|
626
835
|
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
@@ -632,31 +841,32 @@ const handler = createMcpHandler(
|
|
|
632
841
|
"search",
|
|
633
842
|
"Full-text search across the knowledge base.",
|
|
634
843
|
{ query: z.string(), limit: z.number().int().positive().optional() },
|
|
635
|
-
async ({ query, limit }) => json(search(
|
|
844
|
+
async ({ query, limit }) => json(search(index, query, limit)),
|
|
636
845
|
);
|
|
637
846
|
server.tool(
|
|
638
847
|
"get_page",
|
|
639
848
|
"Get a page's full structured content by path.",
|
|
640
849
|
{ path: z.string() },
|
|
641
|
-
async ({ path }) => json(getPage(
|
|
850
|
+
async ({ path }) => json(getPage(index, path)),
|
|
642
851
|
);
|
|
643
852
|
server.tool(
|
|
644
853
|
"list",
|
|
645
854
|
"List knowledge nodes, filtered by kind / tag / entityType.",
|
|
646
855
|
{ kind: z.string().optional(), tag: z.string().optional(), entityType: z.string().optional() },
|
|
647
|
-
async (
|
|
856
|
+
async ({ kind, tag, entityType }) =>
|
|
857
|
+
json(list(index, { kind: kind as KKind | undefined, tag, entityType })),
|
|
648
858
|
);
|
|
649
859
|
server.tool(
|
|
650
860
|
"navigate",
|
|
651
861
|
"Follow relations from a page path / node id / entity ref.",
|
|
652
862
|
{ target: z.string() },
|
|
653
|
-
async ({ target }) => json(navigate(
|
|
863
|
+
async ({ target }) => json(navigate(index, target)),
|
|
654
864
|
);
|
|
655
865
|
server.tool(
|
|
656
866
|
"get_component_data",
|
|
657
867
|
"Get the structured data behind a rendered component (its knowledge face).",
|
|
658
868
|
{ id: z.string() },
|
|
659
|
-
async ({ id }) => json(getComponentData(
|
|
869
|
+
async ({ id }) => json(getComponentData(index, id)),
|
|
660
870
|
);
|
|
661
871
|
},
|
|
662
872
|
{},
|
|
@@ -677,6 +887,27 @@ out
|
|
|
677
887
|
.env*.local
|
|
678
888
|
`
|
|
679
889
|
);
|
|
890
|
+
if (authEnabled) {
|
|
891
|
+
write(
|
|
892
|
+
".env.example",
|
|
893
|
+
`# Auth.js v5 + Google SSO. The gate is OFF until AUTH_GOOGLE_ID is set, so local dev works with
|
|
894
|
+
# this file empty. Copy to .env.local and fill in to enable it on a deploy.
|
|
895
|
+
AUTH_SECRET= # \`openssl rand -base64 32\`
|
|
896
|
+
AUTH_GOOGLE_ID= # presence of this turns the gate ON
|
|
897
|
+
AUTH_GOOGLE_SECRET=
|
|
898
|
+
AUTH_URL= # the deploy's canonical URL, e.g. https://your-kb.vercel.app
|
|
899
|
+
AUTH_TRUST_HOST=true
|
|
900
|
+
${config.auth?.allowedDomain ? `AUTH_ALLOWED_DOMAIN=${config.auth.allowedDomain}` : "# AUTH_ALLOWED_DOMAIN=example.com # restrict sign-in to one workspace domain (optional)"}
|
|
901
|
+
# AUTH_ALLOWED_EMAILS=you@example.com # comma-separated allowlist that bypasses the domain check
|
|
902
|
+
# LOCAL=true # force the gate OFF locally even when configured
|
|
903
|
+
`
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
const authReadme = authEnabled ? `
|
|
907
|
+
|
|
908
|
+
## Auth
|
|
909
|
+
|
|
910
|
+
This KB ships a Google SSO gate (Auth.js v5). It is **off until you set \`AUTH_GOOGLE_ID\`**, so local dev runs open. Copy \`.env.example\` to \`.env.local\` and fill it in to enable it. The gate lives in \`proxy.ts\`, in front of every route \u2014 so the MCP inherits it too.` : "";
|
|
680
911
|
write(
|
|
681
912
|
"README.md",
|
|
682
913
|
`# ${config.name}
|
|
@@ -698,7 +929,417 @@ superlore build
|
|
|
698
929
|
|
|
699
930
|
Config lives in \`superlore.json\`. Author content in \`content/docs/\`.${mcpEnabled ? `
|
|
700
931
|
|
|
701
|
-
The MCP endpoint is served at \`${config.mcp?.path ?? "/api/mcp"}\`.` : ""}
|
|
932
|
+
The MCP endpoint is served at \`${config.mcp?.path ?? "/api/mcp"}\`.` : ""}${authReadme}
|
|
933
|
+
`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
function writeAuth(write, config) {
|
|
937
|
+
const allowedDomain = config.auth?.allowedDomain;
|
|
938
|
+
write(
|
|
939
|
+
"auth.ts",
|
|
940
|
+
`import { createSuperloreAuth } from "superlore/auth";
|
|
941
|
+
|
|
942
|
+
// Auth.js v5 + Google SSO. Allowlists can come from env (AUTH_ALLOWED_DOMAIN / AUTH_ALLOWED_EMAILS)
|
|
943
|
+
// or be passed explicitly here. Off until AUTH_GOOGLE_ID is set, so local dev needs no config.
|
|
944
|
+
export const { handlers, auth, signIn, signOut } = createSuperloreAuth(${allowedDomain ? `{
|
|
945
|
+
allowedDomain: ${JSON.stringify(allowedDomain)},
|
|
946
|
+
}` : "{}"});
|
|
947
|
+
`
|
|
948
|
+
);
|
|
949
|
+
write(
|
|
950
|
+
"app/api/auth/[...nextauth]/route.ts",
|
|
951
|
+
`import { handlers } from "@/auth";
|
|
952
|
+
|
|
953
|
+
export const { GET, POST } = handlers;
|
|
954
|
+
`
|
|
955
|
+
);
|
|
956
|
+
write(
|
|
957
|
+
"proxy.ts",
|
|
958
|
+
`import { auth } from "@/auth";
|
|
959
|
+
import { createAuthProxy } from "superlore/auth";
|
|
960
|
+
|
|
961
|
+
// Next.js 16 middleware lives in proxy.ts. The gate is self-disabling: with no AUTH_GOOGLE_ID
|
|
962
|
+
// (or LOCAL=true) every request passes through, so local dev and public deploys stay open.
|
|
963
|
+
export default createAuthProxy(auth);
|
|
964
|
+
|
|
965
|
+
export const config = {
|
|
966
|
+
// Run on everything except static assets (the helper also skips the auth dance + icons).
|
|
967
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
968
|
+
};
|
|
969
|
+
`
|
|
970
|
+
);
|
|
971
|
+
write(
|
|
972
|
+
"app/auth/signin/page.tsx",
|
|
973
|
+
`import { signIn } from "@/auth";
|
|
974
|
+
|
|
975
|
+
export default async function SignInPage(props: {
|
|
976
|
+
searchParams: Promise<{ callbackUrl?: string }>;
|
|
977
|
+
}) {
|
|
978
|
+
const { callbackUrl } = await props.searchParams;
|
|
979
|
+
return (
|
|
980
|
+
<main className="grid min-h-screen place-items-center p-6">
|
|
981
|
+
<form
|
|
982
|
+
action={async () => {
|
|
983
|
+
"use server";
|
|
984
|
+
await signIn("google", { redirectTo: callbackUrl ?? "/" });
|
|
985
|
+
}}
|
|
986
|
+
className="w-full max-w-sm rounded-lg border border-fd-border bg-fd-card p-6 text-center"
|
|
987
|
+
>
|
|
988
|
+
<h1 className="text-lg font-semibold text-fd-foreground">Sign in</h1>
|
|
989
|
+
<p className="mt-1 text-sm text-fd-muted-foreground">
|
|
990
|
+
This knowledge base is private. Continue with Google to read it.
|
|
991
|
+
</p>
|
|
992
|
+
<button
|
|
993
|
+
type="submit"
|
|
994
|
+
className="mt-5 inline-flex w-full items-center justify-center rounded-md border border-kp-accent-border bg-kp-accent px-4 py-2 text-sm font-medium text-white"
|
|
995
|
+
>
|
|
996
|
+
Continue with Google
|
|
997
|
+
</button>
|
|
998
|
+
</form>
|
|
999
|
+
</main>
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
`
|
|
1003
|
+
);
|
|
1004
|
+
write(
|
|
1005
|
+
"app/auth/error/page.tsx",
|
|
1006
|
+
`export default async function AuthErrorPage(props: {
|
|
1007
|
+
searchParams: Promise<{ error?: string }>;
|
|
1008
|
+
}) {
|
|
1009
|
+
const { error } = await props.searchParams;
|
|
1010
|
+
return (
|
|
1011
|
+
<main className="grid min-h-screen place-items-center p-6">
|
|
1012
|
+
<div className="w-full max-w-sm rounded-lg border border-fd-border bg-fd-card p-6 text-center">
|
|
1013
|
+
<h1 className="text-lg font-semibold text-fd-foreground">Can't sign in</h1>
|
|
1014
|
+
<p className="mt-1 text-sm text-fd-muted-foreground">
|
|
1015
|
+
{error === "AccessDenied"
|
|
1016
|
+
? "That account isn't allowed to access this knowledge base."
|
|
1017
|
+
: "Something went wrong signing in. Try again."}
|
|
1018
|
+
</p>
|
|
1019
|
+
<a
|
|
1020
|
+
href="/auth/signin"
|
|
1021
|
+
className="mt-5 inline-flex w-full items-center justify-center rounded-md border border-fd-border px-4 py-2 text-sm font-medium text-fd-foreground no-underline"
|
|
1022
|
+
>
|
|
1023
|
+
Back to sign in
|
|
1024
|
+
</a>
|
|
1025
|
+
</div>
|
|
1026
|
+
</main>
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
function writePersonalContent(write, config) {
|
|
1033
|
+
write(
|
|
1034
|
+
"content/docs/meta.json",
|
|
1035
|
+
`${JSON.stringify(
|
|
1036
|
+
{
|
|
1037
|
+
title: config.name,
|
|
1038
|
+
root: true,
|
|
1039
|
+
pages: ["index", "beliefs", "working-style", "pr-comments", "voice", "stories"]
|
|
1040
|
+
},
|
|
1041
|
+
null,
|
|
1042
|
+
2
|
|
1043
|
+
)}
|
|
1044
|
+
`
|
|
1045
|
+
);
|
|
1046
|
+
write(
|
|
1047
|
+
"content/docs/index.mdx",
|
|
1048
|
+
`---
|
|
1049
|
+
title: About me
|
|
1050
|
+
description: Who I am, what I care about, and how to work with me.
|
|
1051
|
+
summary: Overview of this person \u2014 role, focus, what they optimize for, and how an agent should use this KB to act on their behalf.
|
|
1052
|
+
tags: [overview, about, profile]
|
|
1053
|
+
---
|
|
1054
|
+
|
|
1055
|
+
<PageHero
|
|
1056
|
+
kicker="Personal KB"
|
|
1057
|
+
title="About me"
|
|
1058
|
+
description="A private, queryable replica of how I think, work, and write. Humans read this; my agents read the same content over MCP and act the way I would."
|
|
1059
|
+
/>
|
|
1060
|
+
|
|
1061
|
+
I'm a placeholder. Replace this with a short, honest description of who you are \u2014 your role, what
|
|
1062
|
+
you build, and the through-line in your work. Keep it concrete: an agent should be able to read
|
|
1063
|
+
this and understand what you'd care about in a decision.
|
|
1064
|
+
|
|
1065
|
+
<KeyFacts
|
|
1066
|
+
items={[
|
|
1067
|
+
{ label: "Role", value: "What you do, in five words or fewer" },
|
|
1068
|
+
{ label: "Focus", value: "The problem you spend most days on" },
|
|
1069
|
+
{ label: "Optimizes for", value: "Speed, correctness, clarity \u2014 pick yours" },
|
|
1070
|
+
{ label: "Time zone", value: "UTC+0" },
|
|
1071
|
+
{ label: "Best reached", value: "Async, in writing" },
|
|
1072
|
+
{ label: "Decides by", value: "Evidence over opinion" },
|
|
1073
|
+
]}
|
|
1074
|
+
/>
|
|
1075
|
+
|
|
1076
|
+
<EntityCard
|
|
1077
|
+
type="person"
|
|
1078
|
+
slug="me"
|
|
1079
|
+
title="Your Name"
|
|
1080
|
+
summary="One line that captures how you'd want an agent to introduce you."
|
|
1081
|
+
icon="user"
|
|
1082
|
+
fields={[
|
|
1083
|
+
{ key: "Discipline", value: "Engineering" },
|
|
1084
|
+
{ key: "Superpower", value: "Turning ambiguity into a plan" },
|
|
1085
|
+
{ key: "Allergic to", value: "Meetings that should have been a doc" },
|
|
1086
|
+
]}
|
|
1087
|
+
refs={[
|
|
1088
|
+
{ rel: "see", target: "/docs/working-style", label: "How I work" },
|
|
1089
|
+
{ rel: "see", target: "/docs/voice", label: "How I write" },
|
|
1090
|
+
]}
|
|
1091
|
+
/>
|
|
1092
|
+
|
|
1093
|
+
## How to use this KB
|
|
1094
|
+
|
|
1095
|
+
Ask it how I'd think about something before you ask me. The pages below are the source of truth
|
|
1096
|
+
for my beliefs, working style, review bar, voice, and the stories that shaped them.
|
|
1097
|
+
`
|
|
1098
|
+
);
|
|
1099
|
+
write(
|
|
1100
|
+
"content/docs/beliefs.mdx",
|
|
1101
|
+
`---
|
|
1102
|
+
title: Beliefs & takes
|
|
1103
|
+
description: The positions I hold and the principles I keep coming back to.
|
|
1104
|
+
summary: This person's strongly-held beliefs and operating principles, each phrased as a position an agent can apply when reasoning on their behalf.
|
|
1105
|
+
tags: [beliefs, principles, takes]
|
|
1106
|
+
---
|
|
1107
|
+
|
|
1108
|
+
<SectionHead
|
|
1109
|
+
eyebrow="What I believe"
|
|
1110
|
+
title="Beliefs & takes"
|
|
1111
|
+
description="Strongly held, loosely coupled. Each is a position you can act on, not a vibe."
|
|
1112
|
+
/>
|
|
1113
|
+
|
|
1114
|
+
These are placeholders \u2014 rewrite them as your own. Phrase each as a clear position so an agent can
|
|
1115
|
+
reason from it.
|
|
1116
|
+
|
|
1117
|
+
<KeyFacts
|
|
1118
|
+
items={[
|
|
1119
|
+
{ label: "On shipping", value: "Small and reversible beats big and perfect." },
|
|
1120
|
+
{ label: "On code", value: "Delete more than you add." },
|
|
1121
|
+
{ label: "On process", value: "Process is scar tissue \u2014 keep only what earned its place." },
|
|
1122
|
+
{ label: "On disagreement", value: "Disagree in the doc, commit in the room." },
|
|
1123
|
+
{ label: "On estimates", value: "Confidence intervals, not single numbers." },
|
|
1124
|
+
{ label: "On tools", value: "Boring tech for load-bearing things." },
|
|
1125
|
+
]}
|
|
1126
|
+
/>
|
|
1127
|
+
|
|
1128
|
+
## A take I'll defend
|
|
1129
|
+
|
|
1130
|
+
<Decision
|
|
1131
|
+
title="Prefer clarity over cleverness"
|
|
1132
|
+
status="accepted"
|
|
1133
|
+
identifier="TAKE-01"
|
|
1134
|
+
context={<>Clever code feels good to write and is expensive to read. Most code is read far more than it is written.</>}
|
|
1135
|
+
decision={<>Optimize for the next person (often future me). If a reviewer has to ask what it does, it isn't done.</>}
|
|
1136
|
+
consequences={[
|
|
1137
|
+
"Fewer abstractions until they pay rent.",
|
|
1138
|
+
"Comments explain why, names explain what.",
|
|
1139
|
+
"I'll trade a few keystrokes for a faster read every time.",
|
|
1140
|
+
]}
|
|
1141
|
+
/>
|
|
1142
|
+
`
|
|
1143
|
+
);
|
|
1144
|
+
write(
|
|
1145
|
+
"content/docs/working-style.mdx",
|
|
1146
|
+
`---
|
|
1147
|
+
title: Working style
|
|
1148
|
+
description: How I plan, decide, focus, and collaborate.
|
|
1149
|
+
summary: How this person works day to day \u2014 how they plan, make decisions, manage focus, and collaborate. Use this to predict how they'd approach a task.
|
|
1150
|
+
tags: [working-style, how-to, collaboration]
|
|
1151
|
+
---
|
|
1152
|
+
|
|
1153
|
+
<SectionHead
|
|
1154
|
+
eyebrow="How I work"
|
|
1155
|
+
title="Working style"
|
|
1156
|
+
description="If you handed me a task, this is the shape of what I'd do with it."
|
|
1157
|
+
/>
|
|
1158
|
+
|
|
1159
|
+
Placeholder content \u2014 make it yours. Be specific enough that an agent could run a task the way you
|
|
1160
|
+
would.
|
|
1161
|
+
|
|
1162
|
+
<FeatureList
|
|
1163
|
+
items={[
|
|
1164
|
+
{ icon: "target", title: "Start from the outcome", description: "I write the goal and the done-condition before touching the work." },
|
|
1165
|
+
{ icon: "split", title: "Decompose, then sequence", description: "Break into reversible steps; do the riskiest cheap thing first." },
|
|
1166
|
+
{ icon: "message-square", title: "Default to writing", description: "A short doc beats a meeting. Decisions live in text, not memory." },
|
|
1167
|
+
{ icon: "gauge", title: "Protect deep work", description: "Mornings are for the hard thing. Coordination batches in the afternoon." },
|
|
1168
|
+
]}
|
|
1169
|
+
/>
|
|
1170
|
+
|
|
1171
|
+
## How I decide
|
|
1172
|
+
|
|
1173
|
+
<Decision
|
|
1174
|
+
title="Two-way doors don't need a meeting"
|
|
1175
|
+
status="accepted"
|
|
1176
|
+
identifier="STYLE-01"
|
|
1177
|
+
context={<>Many decisions are easily reversible. Treating them as if they aren't is the real cost.</>}
|
|
1178
|
+
decision={<>For reversible calls, I pick quickly and move; for one-way doors, I slow down and write the trade-offs out.</>}
|
|
1179
|
+
consequences={[
|
|
1180
|
+
"Speed on the 80% that's reversible.",
|
|
1181
|
+
"Care on the 20% that isn't.",
|
|
1182
|
+
]}
|
|
1183
|
+
/>
|
|
1184
|
+
|
|
1185
|
+
## A day, roughly
|
|
1186
|
+
|
|
1187
|
+
<Schedule
|
|
1188
|
+
label="Typical working day"
|
|
1189
|
+
events={[
|
|
1190
|
+
{ date: "Weekday", time: "09:00", title: "Deep work", body: "The hardest task of the day, no notifications." },
|
|
1191
|
+
{ date: "Weekday", time: "12:30", title: "Reviews & async", body: "PRs, comments, written replies." },
|
|
1192
|
+
{ date: "Weekday", time: "15:00", title: "Collaboration", body: "Pairing, calls, unblock others." },
|
|
1193
|
+
{ date: "Weekday", time: "17:00", title: "Wind-down", body: "Plan tomorrow's first task." },
|
|
1194
|
+
]}
|
|
1195
|
+
/>
|
|
1196
|
+
`
|
|
1197
|
+
);
|
|
1198
|
+
write(
|
|
1199
|
+
"content/docs/pr-comments.mdx",
|
|
1200
|
+
`---
|
|
1201
|
+
title: How I give PR comments
|
|
1202
|
+
description: My review bar, the tone I use, and concrete examples of comments I leave.
|
|
1203
|
+
summary: How this person reviews code \u2014 what they block on versus nudge, the tone of their comments, and worked examples an agent can imitate when reviewing on their behalf.
|
|
1204
|
+
tags: [code-review, feedback, how-to]
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
<SectionHead
|
|
1208
|
+
eyebrow="Code review"
|
|
1209
|
+
title="How I give PR comments"
|
|
1210
|
+
description="What I block on, what I just nudge, and how I phrase it. Replace with your own."
|
|
1211
|
+
/>
|
|
1212
|
+
|
|
1213
|
+
## My review bar
|
|
1214
|
+
|
|
1215
|
+
<Checklist
|
|
1216
|
+
label="What I look for, in order"
|
|
1217
|
+
items={[
|
|
1218
|
+
{ text: "Correctness \u2014 does it do the thing, including the edge cases?", group: "Block on" },
|
|
1219
|
+
{ text: "Tests that would fail without the change", group: "Block on" },
|
|
1220
|
+
{ text: "Names and structure I can read in one pass", group: "Block on" },
|
|
1221
|
+
{ text: "Unnecessary abstraction or dead code", group: "Nudge on" },
|
|
1222
|
+
{ text: "Comments that explain why, not what", group: "Nudge on" },
|
|
1223
|
+
{ text: "Nits \u2014 formatting, ordering, taste", group: "Optional" },
|
|
1224
|
+
]}
|
|
1225
|
+
/>
|
|
1226
|
+
|
|
1227
|
+
## How I phrase comments
|
|
1228
|
+
|
|
1229
|
+
I prefix to signal weight: **blocking:** must change, **suggestion:** take it or leave it,
|
|
1230
|
+
**nit:** ignore freely, **question:** I genuinely don't know. Examples:
|
|
1231
|
+
|
|
1232
|
+
<Example title="A blocking comment">
|
|
1233
|
+
**blocking:** This drops the error on the floor \u2014 if the fetch fails we return \`undefined\` and
|
|
1234
|
+
the caller renders an empty state as if it were success. Surface it, or handle it explicitly.
|
|
1235
|
+
</Example>
|
|
1236
|
+
|
|
1237
|
+
<Example title="A suggestion">
|
|
1238
|
+
**suggestion:** This loop reads cleanly, but \`items.flatMap\` would say the same thing in one line.
|
|
1239
|
+
Your call \u2014 not blocking.
|
|
1240
|
+
</Example>
|
|
1241
|
+
|
|
1242
|
+
<Example title="A nit">
|
|
1243
|
+
**nit:** tiny \u2014 can we name this \`pendingCount\` so it matches the others? Ignore if you disagree.
|
|
1244
|
+
</Example>
|
|
1245
|
+
|
|
1246
|
+
<Tip title="Tone">
|
|
1247
|
+
Critique the code, never the author. Lead with the why. If I'd want it softened when it lands on
|
|
1248
|
+
my own PR, I soften it.
|
|
1249
|
+
</Tip>
|
|
1250
|
+
`
|
|
1251
|
+
);
|
|
1252
|
+
write(
|
|
1253
|
+
"content/docs/voice.mdx",
|
|
1254
|
+
`---
|
|
1255
|
+
title: Voice & writing
|
|
1256
|
+
description: How I sound in writing \u2014 tone, defaults, and what I avoid.
|
|
1257
|
+
summary: This person's writing voice \u2014 tone, structure defaults, and explicit do/don't rules an agent should follow when drafting in their name.
|
|
1258
|
+
tags: [voice, writing, style]
|
|
1259
|
+
---
|
|
1260
|
+
|
|
1261
|
+
<SectionHead
|
|
1262
|
+
eyebrow="How I write"
|
|
1263
|
+
title="Voice & writing"
|
|
1264
|
+
description="So an agent drafting in my name sounds like me, not like a template."
|
|
1265
|
+
/>
|
|
1266
|
+
|
|
1267
|
+
Placeholder \u2014 capture your actual voice. The more specific the do/don't list, the better an agent
|
|
1268
|
+
can match you.
|
|
1269
|
+
|
|
1270
|
+
<KeyFacts
|
|
1271
|
+
items={[
|
|
1272
|
+
{ label: "Tone", value: "Direct, warm, low ceremony" },
|
|
1273
|
+
{ label: "Sentence length", value: "Short. Then one longer one to breathe." },
|
|
1274
|
+
{ label: "Person", value: "First person, active voice" },
|
|
1275
|
+
{ label: "Jargon", value: "Only when it's the precise word" },
|
|
1276
|
+
]}
|
|
1277
|
+
/>
|
|
1278
|
+
|
|
1279
|
+
## Do / don't
|
|
1280
|
+
|
|
1281
|
+
<Comparison
|
|
1282
|
+
caption="My writing defaults"
|
|
1283
|
+
options={["Do", "Don't"]}
|
|
1284
|
+
rows={[
|
|
1285
|
+
{ criterion: "Openers", cells: ["Get to the point in the first line", "Open with filler pleasantries"] },
|
|
1286
|
+
{ criterion: "Hedging", cells: ["Say what I think", "It might possibly be worth considering"] },
|
|
1287
|
+
{ criterion: "Structure", cells: ["Lead with the answer, then the why", "Bury the ask at the end"] },
|
|
1288
|
+
{ criterion: "Emoji", cells: ["Rarely, and never in serious writing", "Decorate every line"] },
|
|
1289
|
+
]}
|
|
1290
|
+
/>
|
|
1291
|
+
|
|
1292
|
+
<Example title="A message in my voice">
|
|
1293
|
+
Shipping the import fix today. It was dropping rows when a column was empty \u2014 now we skip the row
|
|
1294
|
+
and log it. One follow-up: we should validate on upload so this can't happen again. Want me to take
|
|
1295
|
+
that next?
|
|
1296
|
+
</Example>
|
|
1297
|
+
`
|
|
1298
|
+
);
|
|
1299
|
+
write(
|
|
1300
|
+
"content/docs/stories.mdx",
|
|
1301
|
+
`---
|
|
1302
|
+
title: Stories
|
|
1303
|
+
description: Formative moments that explain how I got my defaults.
|
|
1304
|
+
summary: Formative experiences that shaped this person's beliefs and working style, as a dated timeline an agent can reference for context on why they think the way they do.
|
|
1305
|
+
tags: [stories, background, timeline]
|
|
1306
|
+
---
|
|
1307
|
+
|
|
1308
|
+
<SectionHead
|
|
1309
|
+
eyebrow="Where it comes from"
|
|
1310
|
+
title="Stories"
|
|
1311
|
+
description="The moments behind the takes. Replace these with your own \u2014 dates can be approximate."
|
|
1312
|
+
/>
|
|
1313
|
+
|
|
1314
|
+
An agent that knows *why* you believe something reasons better than one that only knows *what*.
|
|
1315
|
+
These are placeholders.
|
|
1316
|
+
|
|
1317
|
+
<Timeline
|
|
1318
|
+
label="Formative moments"
|
|
1319
|
+
items={[
|
|
1320
|
+
{
|
|
1321
|
+
date: "2016",
|
|
1322
|
+
title: "The outage that taught me to write things down",
|
|
1323
|
+
body: "A fix lived only in one person's head. When they were out, we relearned it the hard way. I've defaulted to docs ever since.",
|
|
1324
|
+
status: "done",
|
|
1325
|
+
tags: ["process", "writing"],
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
date: "2019",
|
|
1329
|
+
title: "Shipped small for the first time",
|
|
1330
|
+
body: "Replaced a six-month rewrite with weekly reversible changes. It landed. I stopped believing in big-bang.",
|
|
1331
|
+
status: "done",
|
|
1332
|
+
tags: ["shipping"],
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
date: "2022",
|
|
1336
|
+
title: "A review that changed how I review",
|
|
1337
|
+
body: "Someone critiqued my code without making me feel small. I've tried to give every review that way since.",
|
|
1338
|
+
status: "done",
|
|
1339
|
+
tags: ["code-review", "tone"],
|
|
1340
|
+
},
|
|
1341
|
+
]}
|
|
1342
|
+
/>
|
|
702
1343
|
`
|
|
703
1344
|
);
|
|
704
1345
|
}
|
|
@@ -727,8 +1368,8 @@ async function initCommand(dir, flags) {
|
|
|
727
1368
|
}
|
|
728
1369
|
if (!name) bail("A name is required (pass --name, a [dir] argument, or run interactively).");
|
|
729
1370
|
let type = flags.type;
|
|
730
|
-
if (type && type !== "company-kb" && type !== "product-docs") {
|
|
731
|
-
bail(`Invalid --type "${type}". Use "company-kb"
|
|
1371
|
+
if (type && type !== "company-kb" && type !== "product-docs" && type !== "personal-kb") {
|
|
1372
|
+
bail(`Invalid --type "${type}". Use "company-kb", "product-docs", or "personal-kb".`);
|
|
732
1373
|
}
|
|
733
1374
|
if (!type && interactive) {
|
|
734
1375
|
const answer = await select({
|
|
@@ -743,6 +1384,11 @@ async function initCommand(dir, flags) {
|
|
|
743
1384
|
value: "product-docs",
|
|
744
1385
|
label: "Product docs",
|
|
745
1386
|
hint: "public-facing documentation"
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
value: "personal-kb",
|
|
1390
|
+
label: "Personal KB",
|
|
1391
|
+
hint: "a private, queryable replica of how you think, work, and write"
|
|
746
1392
|
}
|
|
747
1393
|
]
|
|
748
1394
|
});
|
|
@@ -750,6 +1396,7 @@ async function initCommand(dir, flags) {
|
|
|
750
1396
|
type = answer;
|
|
751
1397
|
}
|
|
752
1398
|
if (!type) bail("A type is required (pass --type or run interactively).");
|
|
1399
|
+
const wantsGate = type === "company-kb" || type === "personal-kb";
|
|
753
1400
|
let authEnabled;
|
|
754
1401
|
if (flags.auth !== void 0) {
|
|
755
1402
|
authEnabled = flags.auth;
|
|
@@ -758,12 +1405,12 @@ async function initCommand(dir, flags) {
|
|
|
758
1405
|
} else if (interactive) {
|
|
759
1406
|
const answer = await confirm({
|
|
760
1407
|
message: "Gate the site behind Google SSO (auth)?",
|
|
761
|
-
initialValue:
|
|
1408
|
+
initialValue: wantsGate
|
|
762
1409
|
});
|
|
763
1410
|
if (isCancel(answer)) bail("Cancelled.");
|
|
764
1411
|
authEnabled = answer;
|
|
765
1412
|
} else {
|
|
766
|
-
authEnabled =
|
|
1413
|
+
authEnabled = wantsGate;
|
|
767
1414
|
}
|
|
768
1415
|
let allowedDomain = flags.allowedDomain?.trim();
|
|
769
1416
|
if (authEnabled && !allowedDomain && interactive) {
|
|
@@ -792,7 +1439,7 @@ ${result.issues.map((i) => ` - ${i.path} ${i.message}`).join("\n")}`
|
|
|
792
1439
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
793
1440
|
const targetName = dir ?? (slug || "superlore-kb");
|
|
794
1441
|
const targetDir = resolve3(process.cwd(), targetName);
|
|
795
|
-
if (
|
|
1442
|
+
if (existsSync4(targetDir) && !isEmptyDir(targetDir)) {
|
|
796
1443
|
if (interactive) {
|
|
797
1444
|
const proceed = await confirm({
|
|
798
1445
|
message: `${cyan(targetDir)} is not empty. Scaffold into it anyway?`,
|
|
@@ -805,13 +1452,45 @@ ${result.issues.map((i) => ` - ${i.path} ${i.message}`).join("\n")}`
|
|
|
805
1452
|
}
|
|
806
1453
|
const { root, source } = scaffold({ dir: targetDir, config });
|
|
807
1454
|
outro(`${bold("Scaffolded")} ${config.name} ${dim(`(${source})`)}`);
|
|
808
|
-
if (config.type === "company-kb" && !config.auth?.enabled) {
|
|
1455
|
+
if ((config.type === "company-kb" || config.type === "personal-kb") && !config.auth?.enabled) {
|
|
1456
|
+
const kind = config.type === "personal-kb" ? "personal KB" : "company KB";
|
|
809
1457
|
log.blank();
|
|
810
1458
|
log.warn(
|
|
811
|
-
`This is a ${bold(
|
|
1459
|
+
`This is a ${bold(kind)} but auth is ${bold("not enabled")}. A ${kind} should gate access before you deploy \u2014 re-run with ${cyan("--auth")} or set ${cyan('"auth": { "enabled": true }')} in superlore.json.`
|
|
812
1460
|
);
|
|
813
1461
|
}
|
|
814
1462
|
printNextSteps(root, config);
|
|
1463
|
+
await maybeConnectEditor(flags, interactive);
|
|
1464
|
+
}
|
|
1465
|
+
async function maybeConnectEditor(flags, interactive) {
|
|
1466
|
+
if (flags.connect === false) return;
|
|
1467
|
+
const editors = detectEditors();
|
|
1468
|
+
if (editors.length === 0) {
|
|
1469
|
+
log.blank();
|
|
1470
|
+
log.info(
|
|
1471
|
+
`${dim("Editor preview:")} install VS Code, Cursor, or Windsurf, then ${cyan("superlore connect")}.`
|
|
1472
|
+
);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const names = editors.map((e) => e.label).join(", ");
|
|
1476
|
+
if (flags.connect !== true && !interactive) {
|
|
1477
|
+
log.blank();
|
|
1478
|
+
log.info(
|
|
1479
|
+
`${dim(`Detected ${names}.`)} Run ${cyan("superlore connect")} to install the live-preview extension.`
|
|
1480
|
+
);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
if (flags.connect !== true) {
|
|
1484
|
+
const proceed = await confirm({
|
|
1485
|
+
message: `Install the superlore Preview extension into ${names}?`,
|
|
1486
|
+
initialValue: true
|
|
1487
|
+
});
|
|
1488
|
+
if (isCancel(proceed) || !proceed) {
|
|
1489
|
+
log.info(`${dim("Skipped \u2014 run")} ${cyan("superlore connect")} ${dim("any time.")}`);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
await connectCommand({ optional: true });
|
|
815
1494
|
}
|
|
816
1495
|
function printNextSteps(root, config) {
|
|
817
1496
|
const rel = basename(root);
|
|
@@ -829,14 +1508,17 @@ function printNextSteps(root, config) {
|
|
|
829
1508
|
}
|
|
830
1509
|
|
|
831
1510
|
// src/index.ts
|
|
832
|
-
var VERSION = "0.
|
|
833
|
-
function buildCli(argv =
|
|
1511
|
+
var VERSION = "0.3.0";
|
|
1512
|
+
function buildCli(argv = process3.argv) {
|
|
834
1513
|
const cli = cac("superlore");
|
|
835
|
-
cli.command("init [dir]", "Scaffold a new superlore knowledge base").option("--name <name>", "KB name").option("--type <type>", "KB type: company-kb | product-docs").option("--auth", "Enable the Google SSO auth gate").option("--no-auth", "Disable the auth gate").option("--allowed-domain <domain>", "Restrict SSO to one email domain (implies --auth)").option("--accent <color>", "Brand accent colour (any CSS colour)").option("--no-mcp", "Disable the MCP endpoint (on by default)").option("-y, --yes", "Skip prompts; use flags + defaults").example("superlore init my-kb --type product-docs").example("superlore init acme --type company-kb --auth --allowed-domain acme.com").action(
|
|
1514
|
+
cli.command("init [dir]", "Scaffold a new superlore knowledge base").option("--name <name>", "KB name").option("--type <type>", "KB type: company-kb | product-docs | personal-kb").option("--auth", "Enable the Google SSO auth gate").option("--no-auth", "Disable the auth gate").option("--allowed-domain <domain>", "Restrict SSO to one email domain (implies --auth)").option("--accent <color>", "Brand accent colour (any CSS colour)").option("--no-mcp", "Disable the MCP endpoint (on by default)").option("--connect", "Install the editor extension after scaffolding (skip the prompt)").option("--no-connect", "Don't set up the editor extension").option("-y, --yes", "Skip prompts; use flags + defaults").example("superlore init my-kb --type product-docs").example("superlore init acme --type company-kb --auth --allowed-domain acme.com").example("superlore init me --type personal-kb").action(
|
|
836
1515
|
async (dir, flags) => {
|
|
837
1516
|
const authExplicit = argv.includes("--auth");
|
|
838
1517
|
const noAuthExplicit = argv.includes("--no-auth");
|
|
839
1518
|
const auth = authExplicit ? true : noAuthExplicit ? false : void 0;
|
|
1519
|
+
const connectExplicit = argv.includes("--connect");
|
|
1520
|
+
const noConnectExplicit = argv.includes("--no-connect");
|
|
1521
|
+
const connect = connectExplicit ? true : noConnectExplicit ? false : void 0;
|
|
840
1522
|
await initCommand(dir, {
|
|
841
1523
|
name: flags.name,
|
|
842
1524
|
type: flags.type,
|
|
@@ -845,6 +1527,7 @@ function buildCli(argv = process2.argv) {
|
|
|
845
1527
|
accent: flags.accent,
|
|
846
1528
|
// `--no-mcp` flips this to false; default true is the intended behaviour.
|
|
847
1529
|
mcp: flags.mcp,
|
|
1530
|
+
connect,
|
|
848
1531
|
yes: flags.yes
|
|
849
1532
|
});
|
|
850
1533
|
}
|
|
@@ -855,6 +1538,9 @@ function buildCli(argv = process2.argv) {
|
|
|
855
1538
|
cli.command("build", "Production build of the KB").action(async () => {
|
|
856
1539
|
await buildCommand();
|
|
857
1540
|
});
|
|
1541
|
+
cli.command("connect", "Install the superlore editor extension (VS Code \xB7 Cursor \xB7 Windsurf)").option("--vsix <path>", "Install from a local .vsix instead of the Marketplace").example("superlore connect").action(async (flags) => {
|
|
1542
|
+
await connectCommand({ vsix: flags.vsix });
|
|
1543
|
+
});
|
|
858
1544
|
cli.command("deploy", "Managed deploy (superlore Cloud) \u2014 private beta, joins the waitlist").option("--open", "Open the waitlist URL in your browser").action(async (flags) => {
|
|
859
1545
|
await deployCommand({ open: flags.open });
|
|
860
1546
|
});
|
|
@@ -862,7 +1548,7 @@ function buildCli(argv = process2.argv) {
|
|
|
862
1548
|
cli.version(VERSION);
|
|
863
1549
|
return cli;
|
|
864
1550
|
}
|
|
865
|
-
async function run(argv =
|
|
1551
|
+
async function run(argv = process3.argv) {
|
|
866
1552
|
const cli = buildCli(argv);
|
|
867
1553
|
const tokens = argv.slice(2);
|
|
868
1554
|
const hasCommand = tokens.some((t) => !t.startsWith("-"));
|
|
@@ -878,10 +1564,10 @@ async function run(argv = process2.argv) {
|
|
|
878
1564
|
} catch (error) {
|
|
879
1565
|
const message = error instanceof Error ? error.message : String(error);
|
|
880
1566
|
log.error(message);
|
|
881
|
-
|
|
1567
|
+
process3.exit(1);
|
|
882
1568
|
}
|
|
883
1569
|
}
|
|
884
|
-
var isEntrypoint = Boolean(
|
|
1570
|
+
var isEntrypoint = Boolean(process3.argv[1]) && fileURLToPath2(import.meta.url) === process3.argv[1];
|
|
885
1571
|
if (isEntrypoint) {
|
|
886
1572
|
void run();
|
|
887
1573
|
}
|
package/package.json
CHANGED