@rizom/brain 0.2.0-alpha.3 → 0.2.0-alpha.31

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": "@rizom/brain",
3
- "version": "0.2.0-alpha.3",
3
+ "version": "0.2.0-alpha.31",
4
4
  "description": "Brain runtime + CLI — scaffold, run, and manage AI brain instances",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,14 +19,18 @@
19
19
  "./deploy": {
20
20
  "types": "./dist/deploy.d.ts",
21
21
  "import": "./dist/deploy.js"
22
- }
22
+ },
23
+ "./tsconfig.instance.json": "./tsconfig.instance.json"
23
24
  },
24
25
  "files": [
25
- "dist"
26
+ "dist",
27
+ "templates",
28
+ "tsconfig.instance.json"
26
29
  ],
27
30
  "scripts": {
28
31
  "build": "bun scripts/build.ts",
29
32
  "prepublishOnly": "bun scripts/build.ts",
33
+ "dev:start": "bun dist/brain.js start",
30
34
  "typecheck": "tsc --noEmit",
31
35
  "test": "bun test",
32
36
  "lint": "eslint . --ext .ts"
@@ -61,7 +65,6 @@
61
65
  "@brains/site-default": "workspace:*",
62
66
  "@brains/site-personal": "workspace:*",
63
67
  "@brains/site-professional": "workspace:*",
64
- "@brains/site-rizom": "workspace:*",
65
68
  "@brains/theme-default": "workspace:*",
66
69
  "@brains/theme-rizom": "workspace:*",
67
70
  "@brains/typescript-config": "workspace:*",
@@ -0,0 +1,30 @@
1
+ ARG BUN_VERSION=1.3.10
2
+ FROM oven/bun:${BUN_VERSION}-slim AS runtime
3
+
4
+ WORKDIR /app
5
+
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ curl ca-certificates git \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ ENV XDG_DATA_HOME=/data
11
+ ENV XDG_CONFIG_HOME=/config
12
+ RUN mkdir -p /app/data /app/cache /app/brain-data && \
13
+ chmod -R 777 /app/data /app/cache /app/brain-data
14
+
15
+ EXPOSE 8080
16
+
17
+ CMD ["./node_modules/.bin/brain", "start"]
18
+
19
+ # --- standalone: bake full project into image (brain-cli deploy) ---
20
+ FROM runtime AS standalone
21
+ COPY package.json ./package.json
22
+ RUN bun install --production --ignore-scripts
23
+ COPY . .
24
+
25
+ # --- fleet: install published brain at pinned version (ops deploy) ---
26
+ FROM runtime AS fleet
27
+ ARG BRAIN_VERSION
28
+ RUN test -n "$BRAIN_VERSION" \
29
+ && printf '{"name":"rover-pilot-runtime","private":true}\n' > package.json \
30
+ && bun add @rizom/brain@$BRAIN_VERSION
@@ -0,0 +1,40 @@
1
+ service: __SERVICE_NAME__
2
+ image: <%= ENV['IMAGE_REPOSITORY'] %>
3
+
4
+ servers:
5
+ web:
6
+ hosts:
7
+ - <%= ENV['SERVER_IP'] %>
8
+
9
+ proxy:
10
+ ssl:
11
+ certificate_pem: CERTIFICATE_PEM
12
+ private_key_pem: PRIVATE_KEY_PEM
13
+ hosts:
14
+ - <%= ENV['BRAIN_DOMAIN'] %>
15
+ - <%= ENV['PREVIEW_DOMAIN'] %>
16
+ app_port: 8080
17
+ healthcheck:
18
+ path: /health
19
+
20
+ registry:
21
+ server: ghcr.io
22
+ username: <%= ENV['REGISTRY_USERNAME'] %>
23
+ password:
24
+ - KAMAL_REGISTRY_PASSWORD
25
+
26
+ builder:
27
+ arch: amd64
28
+
29
+ env:
30
+ clear:
31
+ NODE_ENV: production
32
+ secret:
33
+ - AI_API_KEY
34
+ - GIT_SYNC_TOKEN
35
+ - MCP_AUTH_TOKEN
36
+ - DISCORD_BOT_TOKEN
37
+
38
+ volumes:
39
+ - /opt/brain-data:/app/brain-data
40
+ - /opt/brain.yaml:/app/brain.yaml
@@ -0,0 +1,109 @@
1
+ import {
2
+ readJsonResponse,
3
+ requireEnv,
4
+ writeGitHubOutput,
5
+ writeGitHubEnv,
6
+ } from "./helpers";
7
+
8
+ const token = requireEnv("HCLOUD_TOKEN");
9
+ const instanceName = requireEnv("INSTANCE_NAME");
10
+ const sshKeyName = requireEnv("HCLOUD_SSH_KEY_NAME");
11
+ const serverType = requireEnv("HCLOUD_SERVER_TYPE");
12
+ const location = requireEnv("HCLOUD_LOCATION");
13
+
14
+ const headers: Record<string, string> = {
15
+ Authorization: `Bearer ${token}`,
16
+ "Content-Type": "application/json",
17
+ };
18
+ const baseUrl = "https://api.hetzner.cloud/v1";
19
+ const labelSelector = `brain=${instanceName}`;
20
+ const MAX_POLLS = 30;
21
+ const POLL_INTERVAL_MS = 10_000;
22
+
23
+ interface HetznerServer {
24
+ id: number;
25
+ status: string;
26
+ public_net?: { ipv4?: { ip?: string } };
27
+ }
28
+
29
+ function sleep(ms: number): Promise<void> {
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
32
+
33
+ async function listServers(): Promise<HetznerServer[]> {
34
+ const url = `${baseUrl}/servers?label_selector=${encodeURIComponent(labelSelector)}`;
35
+ const response = await fetch(url, { headers });
36
+ const payload = (await readJsonResponse(
37
+ response,
38
+ "Hetzner server lookup",
39
+ )) as {
40
+ servers?: HetznerServer[];
41
+ };
42
+ if (!response.ok || !payload.servers) {
43
+ throw new Error(`Hetzner server lookup failed: ${JSON.stringify(payload)}`);
44
+ }
45
+ return payload.servers;
46
+ }
47
+
48
+ async function createServer(): Promise<HetznerServer> {
49
+ const response = await fetch(`${baseUrl}/servers`, {
50
+ method: "POST",
51
+ headers,
52
+ body: JSON.stringify({
53
+ name: instanceName,
54
+ server_type: serverType,
55
+ image: "ubuntu-22.04",
56
+ location,
57
+ ssh_keys: [sshKeyName],
58
+ labels: { brain: instanceName },
59
+ }),
60
+ });
61
+ const payload = (await readJsonResponse(
62
+ response,
63
+ "Hetzner server create",
64
+ )) as {
65
+ server?: HetznerServer;
66
+ };
67
+ if (!response.ok || !payload.server) {
68
+ throw new Error(`Hetzner server create failed: ${JSON.stringify(payload)}`);
69
+ }
70
+ return payload.server;
71
+ }
72
+
73
+ async function getServer(id: number): Promise<HetznerServer> {
74
+ const response = await fetch(`${baseUrl}/servers/${id}`, { headers });
75
+ const payload = (await readJsonResponse(response, "Hetzner server poll")) as {
76
+ server?: HetznerServer;
77
+ };
78
+ if (!response.ok || !payload.server) {
79
+ throw new Error(`Hetzner server poll failed: ${JSON.stringify(payload)}`);
80
+ }
81
+ return payload.server;
82
+ }
83
+
84
+ let server: HetznerServer | undefined = (await listServers())[0];
85
+ server ??= await createServer();
86
+
87
+ let polls = 0;
88
+ while (server.status !== "running" || !server.public_net?.ipv4?.ip) {
89
+ if (++polls > MAX_POLLS) {
90
+ throw new Error(
91
+ `Server ${server.id} did not become ready after ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s (status: ${server.status})`,
92
+ );
93
+ }
94
+ if (server.status === "error") {
95
+ throw new Error(`Server ${server.id} entered error state`);
96
+ }
97
+ console.log(
98
+ `Waiting for server ${server.id} (status: ${server.status}, poll ${polls}/${MAX_POLLS})...`,
99
+ );
100
+ await sleep(POLL_INTERVAL_MS);
101
+ server = await getServer(server.id);
102
+ }
103
+
104
+ const serverIp = server.public_net.ipv4.ip;
105
+ if (!serverIp) {
106
+ throw new Error(`Server ${server.id} running but has no IPv4 address`);
107
+ }
108
+ writeGitHubOutput("server_ip", serverIp);
109
+ writeGitHubEnv("SERVER_IP", serverIp);
@@ -0,0 +1,55 @@
1
+ import { readJsonResponse, requireEnv } from "./helpers";
2
+
3
+ const token = requireEnv("CF_API_TOKEN");
4
+ const zoneId = requireEnv("CF_ZONE_ID");
5
+ const domain = requireEnv("BRAIN_DOMAIN");
6
+ const serverIp = requireEnv("SERVER_IP");
7
+
8
+ const headers: Record<string, string> = {
9
+ Authorization: `Bearer ${token}`,
10
+ "Content-Type": "application/json",
11
+ };
12
+ const baseUrl = "https://api.cloudflare.com/client/v4";
13
+
14
+ interface CloudflareResult {
15
+ success: boolean;
16
+ result?: Array<{ id: string }>;
17
+ }
18
+
19
+ async function upsertRecord(name: string): Promise<void> {
20
+ const lookupUrl = `${baseUrl}/zones/${zoneId}/dns_records?type=A&name=${encodeURIComponent(name)}`;
21
+ const lookup = await fetch(lookupUrl, { headers });
22
+ const payload = (await readJsonResponse(
23
+ lookup,
24
+ "Cloudflare DNS lookup",
25
+ )) as CloudflareResult;
26
+ if (!lookup.ok || !payload.success) {
27
+ throw new Error(`Cloudflare DNS lookup failed: ${JSON.stringify(payload)}`);
28
+ }
29
+
30
+ const existing = payload.result?.[0];
31
+ const url = existing
32
+ ? `${baseUrl}/zones/${zoneId}/dns_records/${existing.id}`
33
+ : `${baseUrl}/zones/${zoneId}/dns_records`;
34
+
35
+ const response = await fetch(url, {
36
+ method: existing ? "PUT" : "POST",
37
+ headers,
38
+ body: JSON.stringify({
39
+ type: "A",
40
+ name,
41
+ content: serverIp,
42
+ ttl: 1,
43
+ proxied: true,
44
+ }),
45
+ });
46
+ const result = (await readJsonResponse(
47
+ response,
48
+ "Cloudflare DNS upsert",
49
+ )) as CloudflareResult;
50
+ if (!response.ok || !result.success) {
51
+ throw new Error(`Cloudflare DNS upsert failed: ${JSON.stringify(result)}`);
52
+ }
53
+ }
54
+
55
+ await upsertRecord(domain);
@@ -0,0 +1,17 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { requireEnv } from "./helpers";
4
+
5
+ const privateKey = requireEnv("KAMAL_SSH_PRIVATE_KEY");
6
+
7
+ let normalized = privateKey.replace(/\r\n/g, "\n").replace(/\\n/g, "\n");
8
+ if (!normalized.endsWith("\n")) {
9
+ normalized += "\n";
10
+ }
11
+
12
+ const sshDir = join(process.env["HOME"] ?? "/root", ".ssh");
13
+ mkdirSync(sshDir, { recursive: true });
14
+ writeFileSync(join(sshDir, "id_ed25519"), normalized, {
15
+ encoding: "utf8",
16
+ mode: 0o600,
17
+ });
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "display": "Rizom Brain Instance",
4
+ "compilerOptions": {
5
+ "esModuleInterop": true,
6
+ "forceConsistentCasingInFileNames": true,
7
+ "isolatedModules": true,
8
+ "moduleResolution": "bundler",
9
+ "resolveJsonModule": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true,
12
+ "noImplicitAny": true,
13
+ "exactOptionalPropertyTypes": true,
14
+ "noImplicitReturns": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "noUncheckedIndexedAccess": true,
17
+ "noPropertyAccessFromIndexSignature": true,
18
+ "allowUnreachableCode": false,
19
+ "allowUnusedLabels": false,
20
+ "noImplicitOverride": true,
21
+ "preserveWatchOutput": true,
22
+ "skipLibCheck": true,
23
+ "strict": true,
24
+ "target": "ES2022",
25
+ "module": "ESNext",
26
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
27
+ "jsx": "react-jsx",
28
+ "jsxImportSource": "preact",
29
+ "noEmit": true
30
+ }
31
+ }