@brutal-ai/server 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,9 @@
1
- # Temporary Holding Version
1
+ # @brutal-ai/server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [Brutal](https://github.com/cldixon/brutal): feedback from your agents, on your own Cloudflare
4
+ account. This package is the prebuilt Worker, the `defineBrutal` config helper and the
5
+ `brutal-server` CLI. Start with:
6
+
7
+ ```sh
8
+ bun create brutal-ai my-brutal
9
+ ```
package/cli.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // worker/cli/brutal-server.ts
5
+ import { writeFileSync } from "fs";
6
+ import { basename } from "path";
7
+ var {$ } = globalThis.Bun;
8
+
9
+ // worker/config.ts
10
+ import { existsSync, readFileSync } from "fs";
11
+ import { isAbsolute, join } from "path";
12
+ import { bindings, defineConfig } from "cf/config";
13
+ var CONFIG_FILE = "brutal.config.json";
14
+ var DEFAULTS = { worker: "brutal" };
15
+ var ENV = {
16
+ worker: "BRUTAL_WORKER",
17
+ database: "BRUTAL_DATABASE",
18
+ domain: "BRUTAL_DOMAIN"
19
+ };
20
+ function configPath(dir) {
21
+ const named = process.env.BRUTAL_CONFIG;
22
+ return named ? isAbsolute(named) ? named : join(dir, named) : join(dir, CONFIG_FILE);
23
+ }
24
+ function readConfigFile(dir) {
25
+ const path = configPath(dir);
26
+ if (!existsSync(path)) {
27
+ if (process.env.BRUTAL_CONFIG && !process.env.BRUTAL_SETUP) {
28
+ throw new Error(`BRUTAL_CONFIG points at ${path}, which doesn't exist`);
29
+ }
30
+ return null;
31
+ }
32
+ return JSON.parse(readFileSync(path, "utf-8"));
33
+ }
34
+ function loadBrutalConfig(dir, { allowDefaults = false } = {}) {
35
+ const file = readConfigFile(dir);
36
+ const env = {};
37
+ for (const [key, name] of Object.entries(ENV)) {
38
+ const value = process.env[name];
39
+ if (value !== undefined)
40
+ env[key] = value || null;
41
+ }
42
+ if (!file && !env.worker && !allowDefaults && !process.env.BRUTAL_SETUP) {
43
+ throw new Error(`No ${CONFIG_FILE} in ${dir}. Run \`bun run setup\` to make one, ` + "or set BRUTAL_WORKER (and BRUTAL_DOMAIN) in the environment.");
44
+ }
45
+ const config = { ...DEFAULTS, ...file, ...env };
46
+ if (!config.worker)
47
+ throw new Error(`${CONFIG_FILE} needs a worker name`);
48
+ return config;
49
+ }
50
+
51
+ // worker/cli/brutal-server.ts
52
+ var root = process.cwd();
53
+ var [command, ...rest] = process.argv.slice(2);
54
+ process.env.CF_SEND_TELEMETRY ??= "false";
55
+ var commands = { setup, deploy, secrets, dev };
56
+ var run = command ? commands[command] : undefined;
57
+ if (!run) {
58
+ console.log("Usage: brutal-server <setup|deploy|secrets|dev>");
59
+ process.exit(command ? 1 : 0);
60
+ }
61
+ await run();
62
+ async function requireAuth() {
63
+ const whoami = await $`bunx cf auth whoami`.nothrow().quiet();
64
+ let authenticated = false;
65
+ try {
66
+ authenticated = whoami.exitCode === 0 && JSON.parse(whoami.stdout.toString()).authenticated;
67
+ } catch {}
68
+ if (!authenticated) {
69
+ console.error("Not signed in to Cloudflare. Run: bunx cf auth login");
70
+ process.exit(1);
71
+ }
72
+ }
73
+ async function setup() {
74
+ console.log(`
75
+ Brutal setup
76
+ `);
77
+ process.env.BRUTAL_SETUP = "1";
78
+ await requireAuth();
79
+ const current = readConfigFile(root) ?? {};
80
+ const worker = prompt(`Worker name [${current.worker ?? DEFAULTS.worker}]:`)?.trim();
81
+ const domainAnswer = prompt(`Custom domain, on a zone in your Cloudflare account (blank for none, "-" to remove) [${current.domain ?? ""}]:`)?.trim();
82
+ const domain = domainAnswer === "-" ? "" : domainAnswer || current.domain || "";
83
+ const config = {
84
+ ...current,
85
+ worker: worker || current.worker || DEFAULTS.worker,
86
+ domain: domain || null
87
+ };
88
+ writeFileSync(configPath(root), `${JSON.stringify(config, null, 2)}
89
+ `);
90
+ console.log(`
91
+ \u2713 Wrote ${basename(configPath(root))}`);
92
+ console.log(domain ? `\u2713 ${domain} will route to this Worker (its zone must be on your account)` : "\u2713 No custom domain: it will be served from *.workers.dev");
93
+ console.log(`
94
+ Next: bun run deploy
95
+ `);
96
+ }
97
+ async function workerExists(worker) {
98
+ return (await $`bunx cf workers secrets list --worker ${worker}`.nothrow().quiet()).exitCode === 0;
99
+ }
100
+ async function deploy() {
101
+ const { worker } = loadBrutalConfig(root);
102
+ const existed = await workerExists(worker);
103
+ const code = await interactive(["bunx", "cf", "deploy", ...rest]);
104
+ if (code !== 0)
105
+ process.exit(code);
106
+ if (existed || rest.includes("--dry-run"))
107
+ return;
108
+ if (process.env.CI) {
109
+ console.log(`
110
+ No setup code was set here, since CI logs may be public.`);
111
+ console.log("Run `bun run secrets` on your own machine for one before you claim it.\n");
112
+ return;
113
+ }
114
+ await secrets();
115
+ }
116
+ async function secrets() {
117
+ const { worker } = loadBrutalConfig(root);
118
+ const bytes = crypto.getRandomValues(new Uint8Array(9));
119
+ const setupCode = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
120
+ await $`bunx cf workers secrets update SETUP_CODE --worker ${worker} --type secret_text --text ${setupCode}`.quiet();
121
+ console.log(`
122
+ \u2713 SETUP_CODE set on ${worker}`);
123
+ console.log(`
124
+ Setup code: ${setupCode}`);
125
+ console.log(`Open /setup on your instance and enter it to make your keys.
126
+ `);
127
+ }
128
+ async function dev() {
129
+ process.exit(await interactive(["bunx", "cf", "dev", "--port", "8787", ...rest]));
130
+ }
131
+ function interactive(cmd) {
132
+ return Bun.spawn(cmd, { cwd: root, stdio: ["inherit", "inherit", "inherit"] }).exited;
133
+ }
package/config.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /** A Brutal instance's settings, as brutal.config.json holds them. */
2
+ export type BrutalConfig = {
3
+ /** Worker name. Also names the D1 database unless `database` is set. */
4
+ worker: string;
5
+ /** D1 database for reports and keys. Defaults to the Worker name. */
6
+ database?: string;
7
+ /** Custom domain on a zone in your Cloudflare account. Without one, workers.dev serves it. */
8
+ domain?: string | null;
9
+ };
10
+
11
+ /**
12
+ * The Brutal Worker's cf config, from the brutal.config.json in `dir`. Use as the default export
13
+ * of cloudflare.config.ts:
14
+ *
15
+ * export default defineBrutal({ dir: import.meta.dirname });
16
+ */
17
+ export function defineBrutal(options: { dir: string; entrypoint?: string }): unknown;
18
+
19
+ export function loadBrutalConfig(dir: string, options?: { allowDefaults?: boolean }): BrutalConfig;
package/config.js ADDED
@@ -0,0 +1,73 @@
1
+ // worker/config.ts
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { bindings, defineConfig } from "cf/config";
5
+ var CONFIG_FILE = "brutal.config.json";
6
+ var DEFAULTS = { worker: "brutal" };
7
+ var ENV = {
8
+ worker: "BRUTAL_WORKER",
9
+ database: "BRUTAL_DATABASE",
10
+ domain: "BRUTAL_DOMAIN"
11
+ };
12
+ function configPath(dir) {
13
+ const named = process.env.BRUTAL_CONFIG;
14
+ return named ? isAbsolute(named) ? named : join(dir, named) : join(dir, CONFIG_FILE);
15
+ }
16
+ function readConfigFile(dir) {
17
+ const path = configPath(dir);
18
+ if (!existsSync(path)) {
19
+ if (process.env.BRUTAL_CONFIG && !process.env.BRUTAL_SETUP) {
20
+ throw new Error(`BRUTAL_CONFIG points at ${path}, which doesn't exist`);
21
+ }
22
+ return null;
23
+ }
24
+ return JSON.parse(readFileSync(path, "utf-8"));
25
+ }
26
+ function loadBrutalConfig(dir, { allowDefaults = false } = {}) {
27
+ const file = readConfigFile(dir);
28
+ const env = {};
29
+ for (const [key, name] of Object.entries(ENV)) {
30
+ const value = process.env[name];
31
+ if (value !== undefined)
32
+ env[key] = value || null;
33
+ }
34
+ if (!file && !env.worker && !allowDefaults && !process.env.BRUTAL_SETUP) {
35
+ throw new Error(`No ${CONFIG_FILE} in ${dir}. Run \`bun run setup\` to make one, ` + "or set BRUTAL_WORKER (and BRUTAL_DOMAIN) in the environment.");
36
+ }
37
+ const config = { ...DEFAULTS, ...file, ...env };
38
+ if (!config.worker)
39
+ throw new Error(`${CONFIG_FILE} needs a worker name`);
40
+ return config;
41
+ }
42
+ function defineBrutal({ dir, entrypoint }) {
43
+ return defineConfig(() => {
44
+ const argv = process.argv;
45
+ const brutal = loadBrutalConfig(dir, {
46
+ allowDefaults: argv.includes("dev") || argv.includes("types")
47
+ });
48
+ const domain = brutal.domain || null;
49
+ return {
50
+ worker: {
51
+ name: brutal.worker,
52
+ compatibilityDate: "2026-10-01",
53
+ compatibilityFlags: ["nodejs_compat"],
54
+ entrypoint: entrypoint ?? join(import.meta.dirname, "worker", "index.js"),
55
+ observability: { enabled: true },
56
+ ...domain && { domains: [domain] },
57
+ workersDev: !domain,
58
+ previewUrls: false,
59
+ env: {
60
+ DB: bindings.d1({ name: brutal.database ?? brutal.worker })
61
+ }
62
+ }
63
+ };
64
+ });
65
+ }
66
+ export {
67
+ CONFIG_FILE,
68
+ DEFAULTS,
69
+ configPath,
70
+ defineBrutal,
71
+ loadBrutalConfig,
72
+ readConfigFile
73
+ };
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
+ "version": "0.1.0",
3
+ "license": "MIT",
2
4
  "name": "@brutal-ai/server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
5
+ "description": "Brutal: feedback from your agents, deployed to your own Cloudflare account",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cldixon/brutal.git",
10
+ "directory": "worker"
11
+ },
12
+ "bin": {
13
+ "brutal-server": "cli.js"
14
+ },
15
+ "exports": {
16
+ "./config": {
17
+ "types": "./config.d.ts",
18
+ "default": "./config.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "cli.js",
23
+ "config.js",
24
+ "config.d.ts",
25
+ "worker",
26
+ "README.md"
27
+ ],
28
+ "peerDependencies": {
29
+ "cf": "1.0.0-beta.28",
30
+ "wrangler": "^4.149.0"
31
+ },
32
+ "engines": {
33
+ "bun": ">=1.2.0"
34
+ }
35
+ }