Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"lithium-kit": "./dist/index.js"
},
"files": [
"dist"
"dist",
"templates"
],
"scripts": {
"build": "tsup",
Expand Down
134 changes: 134 additions & 0 deletions packages/kit/src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
copyFileSync,
} from "fs";
import { resolve, dirname } from "path";
import * as p from "@clack/prompts";
import { z } from "zod";

const AdapterSchema = z.enum(["postgres", "drizzle"]);
type Adapter = z.infer<typeof AdapterSchema>;

const ADAPTER_CONFIG: Record<
Adapter,
{
deps: Record<string, string>;
schema: string;
migrate: string;
}
> = {
postgres: {
deps: {
"@lithium-ai/core": "latest",
"@lithium-ai/postgres": "latest",
"@lithium-ai/mcp": "latest",
postgres: "^3.4.9",
dotenv: "^16.5.0",
},
schema: "schema.sql",
migrate: "psql $LITHIUM_DATABASE_URL -f lithium/schema.sql",
},
drizzle: {
deps: {
"@lithium-ai/core": "latest",
"@lithium-ai/drizzle": "latest",
"@lithium-ai/mcp": "latest",
"drizzle-orm": ">=0.30.0",
postgres: "^3.4.9",
dotenv: "^16.5.0",
},
schema: "schema.ts",
migrate: "npx drizzle-kit push",
},
};

function parseAdapterFlag(): Adapter | null {
const idx = process.argv.indexOf("--adapter");
if (idx === -1) return null;

const result = AdapterSchema.safeParse(process.argv[idx + 1]);
if (!result.success) {
throw new Error(
"Invalid adapter. Use --adapter postgres or --adapter drizzle"
);
}

return result.data;
}

async function promptAdapter(): Promise<Adapter> {
const selected = await p.select({
message: "Which adapter?",
options: [
{ value: "postgres" as const, label: "Postgres (postgres.js)" },
{ value: "drizzle" as const, label: "Drizzle ORM" },
],
});

if (p.isCancel(selected)) {
p.cancel("Cancelled.");
process.exit(0);
}

return selected;
}

export async function init(): Promise<void> {
try {
const cwd = process.cwd();
const templates = resolve(dirname(import.meta.dirname), "templates");

p.intro("@lithium-ai/kit init");

if (existsSync(resolve(cwd, "lithium.config.json"))) {
p.log.error("lithium.config.json already exists.");
p.outro("Nothing to do.");
process.exit(1);
}

const adapter = parseAdapterFlag() ?? (await promptAdapter());
const config = ADAPTER_CONFIG[adapter];

mkdirSync(resolve(cwd, "lithium"), { recursive: true });
copyFileSync(
resolve(templates, adapter, "server.ts.tmpl"),
resolve(cwd, "lithium", "server.ts")
);
copyFileSync(
resolve(templates, adapter, `${config.schema}.tmpl`),
resolve(cwd, "lithium", config.schema)
);

writeFileSync(
resolve(cwd, "lithium.config.json"),
JSON.stringify({ mcpServer: "lithium/server.ts" }, null, 2) + "\n"
);

const pkgPath = resolve(cwd, "package.json");
if (!existsSync(pkgPath)) throw new Error("package.json not found.");
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
pkg.dependencies = { ...pkg.dependencies, ...config.deps };
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");

p.log.success(`Scaffolded lithium/ with ${adapter} adapter`);

p.note(
[
"1. Install deps: pnpm install",
"2. Add LITHIUM_DATABASE_URL to .env",
`3. Run migrations: ${config.migrate}`,
"4. Start server: npx @lithium-ai/kit serve",
"5. Connect: claude mcp add lithium -- npx @lithium-ai/kit serve",
].join("\n"),
"Next steps"
);

p.outro("Done.");
} catch (error) {
p.log.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
25 changes: 18 additions & 7 deletions packages/kit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import * as p from "@clack/prompts";
import { serve } from "./commands/serve";
import { init } from "./commands/init";

const command = process.argv[2];

if (command === "serve") {
serve();
} else {
p.intro("@lithium-ai/kit");
p.log.message("serve Start the MCP server");
p.log.step("npx @lithium-ai/kit serve");
p.outro("https://github.com/0xJaksun/lithium-core");
switch (command) {
case "init":
init();
break;
case "serve":
serve();
break;
default:
p.intro("@lithium-ai/kit");
p.log.message(
[
"init Scaffold Lithium into your project",
"serve Start the MCP server",
].join("\n")
);
p.outro("https://github.com/0xJaksun/lithium-core");
break;
}
1 change: 1 addition & 0 deletions packages/kit/templates/drizzle/schema.ts.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { clusters, entries, entryVersions } from "@lithium-ai/drizzle";
16 changes: 16 additions & 0 deletions packages/kit/templates/drizzle/server.ts.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Lithium } from "@lithium-ai/core";
import { drizzleAdapter } from "@lithium-ai/drizzle";
import { serveMcp } from "@lithium-ai/mcp";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

if (!process.env.LITHIUM_DATABASE_URL) {
console.error("LITHIUM_DATABASE_URL is required. Add it to your .env file.");
process.exit(1);
}

const sql = postgres(process.env.LITHIUM_DATABASE_URL);
const db = drizzle(sql);
const lithium = new Lithium(drizzleAdapter(db));

serveMcp(lithium);
32 changes: 32 additions & 0 deletions packages/kit/templates/postgres/schema.sql.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
CREATE EXTENSION IF NOT EXISTS ltree;

CREATE TABLE clusters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_id UUID REFERENCES clusters(id) ON DELETE CASCADE,
path ltree NOT NULL,
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_clusters_path UNIQUE (path)
);

CREATE INDEX idx_clusters_path_gist ON clusters USING gist (path);
CREATE INDEX idx_clusters_parent_id ON clusters (parent_id);

CREATE TABLE entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_entries_cluster_id ON entries (cluster_id);

CREATE TABLE entry_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entry_id UUID NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
version INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_entry_versions_entry_version UNIQUE (entry_id, version)
);

CREATE INDEX idx_entry_versions_entry_id ON entry_versions (entry_id);
14 changes: 14 additions & 0 deletions packages/kit/templates/postgres/server.ts.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Lithium } from "@lithium-ai/core";
import { postgresAdapter } from "@lithium-ai/postgres";
import { serveMcp } from "@lithium-ai/mcp";
import postgres from "postgres";

if (!process.env.LITHIUM_DATABASE_URL) {
console.error("LITHIUM_DATABASE_URL is required. Add it to your .env file.");
process.exit(1);
}

const sql = postgres(process.env.LITHIUM_DATABASE_URL);
const lithium = new Lithium(postgresAdapter(sql));

serveMcp(lithium);
Loading