benchworks 0.0.1 → 0.2.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.
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Benchworks' runner. `benchworks connect --code XXXX-XXXX` pairs this Mac with your Benchworks account, and keeps it
4
+ * connected while this Terminal window is open. It checks what the Assistant needs (Claude Code, its login, git), finds
5
+ * the git repositories in the usual folders, and reports both to benchworks.dev every few seconds. When you ask the
6
+ * Assistant something on benchworks.dev, it runs your own Claude Code here, under Benchworks' rules (not your personal
7
+ * settings or MCP servers), and sends back what Claude says and does; anything that would change a file or run a
8
+ * command waits for your Allow on the page. Run it again without a code and it reconnects as the same Mac.
9
+ */
10
+ import { execFile, spawn } from "node:child_process";
11
+ import { request as httpRequest } from "node:http";
12
+ import { request as httpsRequest } from "node:https";
13
+ import { createInterface } from "node:readline";
14
+ import { chmod, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
15
+ import { homedir, hostname, platform } from "node:os";
16
+ import { join } from "node:path";
17
+ import { promisify } from "node:util";
18
+
19
+ const VERSION = "0.2.0";
20
+ const API = (process.env.BENCHWORKS_URL ?? "https://benchworks.dev").replace(/\/+$/, "");
21
+ const HOME = homedir();
22
+ const STATE = join(HOME, ".benchworks", "runner.json");
23
+ const EVERY_MS = 5_000;
24
+ const REPOS_EVERY_MS = 60_000;
25
+
26
+ const run = promisify(execFile);
27
+ const say = (line = "") => process.stdout.write(`${line}\n`);
28
+ const dim = (s) => (process.stdout.isTTY ? `\x1b[2m${s}\x1b[0m` : s);
29
+ const ok = (s) => (process.stdout.isTTY ? `\x1b[32m${s}\x1b[0m` : s);
30
+ const warn = (s) => (process.stdout.isTTY ? `\x1b[33m${s}\x1b[0m` : s);
31
+
32
+ async function out(cmd, args, cwd) {
33
+ try {
34
+ const { stdout } = await run(cmd, args, { cwd, timeout: 8_000, env: process.env });
35
+ return stdout.trim();
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ const tilde = (p) => (p.startsWith(HOME) ? `~${p.slice(HOME.length)}` : p);
42
+ const untilde = (p) => (p.startsWith("~/") ? join(HOME, p.slice(2)) : p);
43
+
44
+ /* ---------------------------------------------------------------- what the Assistant needs */
45
+
46
+ async function checks() {
47
+ const claudeOut = await out("claude", ["--version"]);
48
+ const claude = claudeOut ? { ok: true, detail: claudeOut.split(/\s+/)[0] ?? claudeOut } : { ok: false, detail: "" };
49
+ let login = { ok: false, detail: "" };
50
+ if (claude.ok) {
51
+ // who Claude Code is signed in as, from its own settings; only the address is read
52
+ try {
53
+ const settings = JSON.parse(await readFile(join(HOME, ".claude.json"), "utf8"));
54
+ const email = settings?.oauthAccount?.emailAddress;
55
+ if (typeof email === "string" && email) login = { ok: true, detail: email };
56
+ } catch {
57
+ /* no settings file: not signed in */
58
+ }
59
+ if (!login.ok && process.env.ANTHROPIC_API_KEY) login = { ok: true, detail: "with an API key" };
60
+ }
61
+ const gitOut = await out("git", ["--version"]);
62
+ const git = gitOut ? { ok: true, detail: gitOut.replace(/^git version\s*/, "").split(/\s+/)[0] } : { ok: false, detail: "" };
63
+ return { claude, login, git };
64
+ }
65
+
66
+ /* ---------------------------------------------------------------- the repositories on this Mac */
67
+
68
+ const SKIP = new Set(["Library", "Applications", "Movies", "Music", "Pictures", "Public", "Downloads", "node_modules"]);
69
+
70
+ async function dirs(path) {
71
+ try {
72
+ const entries = await readdir(path, { withFileTypes: true });
73
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP.has(e.name)).slice(0, 200).map((e) => join(path, e.name));
74
+ } catch {
75
+ return [];
76
+ }
77
+ }
78
+
79
+ const isRepo = (path) => stat(join(path, ".git")).then(() => true, () => false);
80
+
81
+ /** Git repositories in the home folder and one level down, and in ~/Documents/GitHub; newest work first. */
82
+ async function repos() {
83
+ const found = new Set();
84
+ for (const top of await dirs(HOME)) {
85
+ if (await isRepo(top)) found.add(top);
86
+ else for (const inner of await dirs(top)) if (await isRepo(inner)) found.add(inner);
87
+ }
88
+ for (const inner of await dirs(join(HOME, "Documents", "GitHub"))) if (await isRepo(inner)) found.add(inner);
89
+ const list = await Promise.all(
90
+ [...found].map(async (path) => ({
91
+ name: path.split("/").pop(),
92
+ path: tilde(path),
93
+ branch: (await out("git", ["rev-parse", "--abbrev-ref", "HEAD"], path)) ?? "",
94
+ at: (await out("git", ["log", "-1", "--format=%cI"], path)) || null,
95
+ })),
96
+ );
97
+ return list.sort((a, b) => (b.at ?? "").localeCompare(a.at ?? "")).slice(0, 20);
98
+ }
99
+
100
+ /* ---------------------------------------------------------------- talking to Benchworks */
101
+
102
+ async function machine() {
103
+ if (platform() === "darwin") {
104
+ const name = await out("scutil", ["--get", "ComputerName"]);
105
+ if (name) return name;
106
+ }
107
+ return hostname();
108
+ }
109
+
110
+ async function api(path, body, key) {
111
+ const res = await fetch(`${API}${path}`, {
112
+ method: "POST",
113
+ headers: { "content-type": "application/json", "user-agent": `benchworks-runner/${VERSION}`, ...(key ? { authorization: `Bearer ${key}` } : {}) },
114
+ body: JSON.stringify(body),
115
+ signal: AbortSignal.timeout(15_000),
116
+ });
117
+ const answer = await res.json().catch(() => ({}));
118
+ return { status: res.status, body: answer };
119
+ }
120
+
121
+ /**
122
+ * The long poll, on a connection of its own: through fetch's shared pool, a request held open for 25 seconds made the
123
+ * reports and the events wait behind it.
124
+ */
125
+ function get(path, key) {
126
+ const url = new URL(`${API}${path}`);
127
+ const send = url.protocol === "http:" ? httpRequest : httpsRequest;
128
+ return new Promise((resolve, reject) => {
129
+ const req = send(url, { headers: { "user-agent": `benchworks-runner/${VERSION}`, authorization: `Bearer ${key}` }, agent: false, timeout: 40_000 }, (res) => {
130
+ let raw = "";
131
+ res.setEncoding("utf8");
132
+ res.on("data", (d) => (raw += d));
133
+ res.on("end", () => {
134
+ let body = {};
135
+ try {
136
+ body = JSON.parse(raw);
137
+ } catch {
138
+ /* not JSON: an error page on the way */
139
+ }
140
+ resolve({ status: res.statusCode ?? 0, body });
141
+ });
142
+ });
143
+ req.on("timeout", () => req.destroy(new Error("timeout")));
144
+ req.on("error", reject);
145
+ req.end();
146
+ });
147
+ }
148
+
149
+ const WHY = {
150
+ no_code: "That code does not work. Make a new one on benchworks.dev and run the command again.",
151
+ used: "That code was used already. Make a new one on benchworks.dev and run the command again.",
152
+ expired: "That code ran out. Make a new one on benchworks.dev and run the command again.",
153
+ too_many_tries: "Too many codes that did not work. Wait ten minutes, then try a new one.",
154
+ };
155
+
156
+ async function readState() {
157
+ try {
158
+ return JSON.parse(await readFile(STATE, "utf8"));
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ async function saveState(state) {
165
+ await mkdir(join(HOME, ".benchworks"), { recursive: true, mode: 0o700 });
166
+ await writeFile(STATE, JSON.stringify(state, null, 2), { mode: 0o600 });
167
+ await chmod(STATE, 0o600);
168
+ }
169
+
170
+ /** The folder picker the page asked for: the Mac's own, through AppleScript. Null when it was cancelled. */
171
+ async function pickFolder() {
172
+ const path = await out("osascript", ["-e", 'POSIX path of (choose folder with prompt "Pick the folder Benchworks should work in")']);
173
+ return path ? tilde(path.replace(/\/$/, "")) : null;
174
+ }
175
+
176
+ function printChecks(c, before) {
177
+ const lines = [
178
+ ["claude", c.claude.ok ? `Claude Code ${c.claude.detail}` : "Claude Code is not on this Mac. Install it: npm install -g @anthropic-ai/claude-code"],
179
+ ["login", c.login.ok ? `Signed in to Claude Code as ${c.login.detail}` : c.claude.ok ? "Claude Code is not signed in. Run claude and sign in with /login" : null],
180
+ ["git", c.git.ok ? `git ${c.git.detail}` : "git is not on this Mac. Install Apple's command line tools: xcode-select --install"],
181
+ ];
182
+ for (const [id, line] of lines) {
183
+ if (!line) continue;
184
+ if (before && before[id]?.ok === c[id].ok && before[id]?.detail === c[id].detail) continue;
185
+ say(` ${c[id].ok ? ok("✓") : warn("✗")} ${line}`);
186
+ }
187
+ }
188
+
189
+ async function connect(code) {
190
+ say(`Benchworks runner ${VERSION}`);
191
+ if (platform() !== "darwin") say(warn("Benchworks runs on macOS for now; this may not work here."));
192
+ let found = await repos();
193
+ let foundAt = Date.now();
194
+ let c = await checks();
195
+ const report = () => ({ version: VERSION, platform: platform(), checks: c, repos: found });
196
+
197
+ let state = await readState();
198
+ if (code) {
199
+ const name = await machine();
200
+ const paired = await api("/api/runner/pair", { code, machine: name, report: report() }).catch(() => null);
201
+ if (!paired) return fail(`Cannot reach ${API}. Check the connection and run the command again.`);
202
+ if (paired.status !== 201) return fail(WHY[paired.body.error] ?? `Benchworks said no (${paired.body.error ?? paired.status}).`);
203
+ state = { url: API, runnerId: paired.body.runnerId, key: paired.body.token, email: paired.body.email, machine: name, again: paired.body.again };
204
+ await saveState(state);
205
+ say(`Connected ${name} to Benchworks as ${paired.body.email}.`);
206
+ } else if (!state?.key) {
207
+ return fail("This Mac is not connected yet. Copy the command with its code from benchworks.dev and run it here.");
208
+ } else {
209
+ say(`Reconnecting ${state.machine} as ${state.email}.`);
210
+ }
211
+ printChecks(c, null);
212
+ key = state.key;
213
+ known = found;
214
+ void jobs();
215
+ again = state.again ?? again;
216
+ say(dim(`Keep this window open while you work. Ctrl-C disconnects; to come back, run: ${again}`));
217
+
218
+ let away = false;
219
+ let picked;
220
+ for (;;) {
221
+ const before = c;
222
+ c = await checks();
223
+ printChecks(c, before);
224
+ if (Date.now() - foundAt > REPOS_EVERY_MS) {
225
+ found = await repos();
226
+ known = found;
227
+ foundAt = Date.now();
228
+ }
229
+ const body = { report: report(), ...(picked !== undefined ? { picked } : {}) };
230
+ picked = undefined;
231
+ const answer = await api("/api/runner/report", body, state.key).catch((e) => ({ status: 0, body: {}, why: e.cause?.code ?? e.name ?? "error" }));
232
+ if (answer.status === 0 || answer.status >= 500) {
233
+ if (!away) say(warn(`Cannot reach ${API} (${answer.why ?? answer.status}). Trying again.`));
234
+ away = true;
235
+ } else if (answer.status === 401) {
236
+ fail("Benchworks no longer knows this Mac. Make a new code on benchworks.dev and run the command with it.");
237
+ return quit(1);
238
+ } else {
239
+ if (away) say("Back in touch with Benchworks.");
240
+ away = false;
241
+ if (answer.body.ask === "folder") {
242
+ say("Benchworks asked for a folder. The picker is open.");
243
+ const path = await pickFolder();
244
+ picked = path ?? "";
245
+ say(path ? ` Working in ${path}` : " No folder picked.");
246
+ continue;
247
+ }
248
+ }
249
+ await new Promise((r) => setTimeout(r, EVERY_MS));
250
+ }
251
+ }
252
+
253
+ /* ---------------------------------------------------------------- conversations */
254
+
255
+ const SPACE = join(HOME, ".benchworks", "space");
256
+ const IDLE_MS = 15 * 60_000;
257
+ let key = null;
258
+ let known = [];
259
+
260
+ /** Benchworks' rules for a conversation: where it starts, the person's repositories, and how to ask before changing. */
261
+ function rules(found) {
262
+ return [
263
+ "You are the Benchworks Assistant. You run on the person's own Mac, through their own Claude Code, started from benchworks.dev.",
264
+ `You start in Benchworks' own folder, ${SPACE}. The person's git repositories on this Mac, newest work first:`,
265
+ ...found.map((r) => `- ${r.name}: ${untilde(r.path)}${r.branch ? ` (on ${r.branch})` : ""}`),
266
+ "When they name one, work in it by its full path. Reading them needs no permission; changing a file or running a command asks the person first, on the page.",
267
+ "Answer plainly and briefly: the person reads you in a browser.",
268
+ ].join("\n");
269
+ }
270
+
271
+ /** One line for what a tool does, as the page and the permission card show it. */
272
+ function summarize(name, input = {}) {
273
+ const path = (p) => (typeof p === "string" ? tilde(p) : "");
274
+ switch (name) {
275
+ case "Bash":
276
+ return String(input.command ?? "").slice(0, 300);
277
+ case "Read":
278
+ case "Edit":
279
+ case "Write":
280
+ case "NotebookEdit":
281
+ return path(input.file_path ?? input.notebook_path);
282
+ case "Grep":
283
+ return `${input.pattern ?? ""}${input.path ? ` in ${path(input.path)}` : ""}`;
284
+ case "Glob":
285
+ return String(input.pattern ?? "");
286
+ case "WebFetch":
287
+ return String(input.url ?? "");
288
+ case "WebSearch":
289
+ return String(input.query ?? "");
290
+ default:
291
+ return JSON.stringify(input).slice(0, 200);
292
+ }
293
+ }
294
+
295
+ const sessions = new Map();
296
+ const outbox = [];
297
+ const acks = [];
298
+ let flushing = null;
299
+ let flushTimer = null;
300
+
301
+ /** Events go to Benchworks in small batches: pieces of text every 150 ms, anything else at once. */
302
+ function emit(event, now = false) {
303
+ outbox.push(event);
304
+ if (now) void flush();
305
+ else if (!flushTimer) flushTimer = setTimeout(() => void flush(), 150);
306
+ }
307
+
308
+ async function flush() {
309
+ if (flushTimer) clearTimeout(flushTimer);
310
+ flushTimer = null;
311
+ if (flushing) return flushing.then(() => flush());
312
+ if (!outbox.length && !acks.length) return;
313
+ const events = outbox.splice(0);
314
+ const done = acks.splice(0);
315
+ flushing = api("/api/runner/events", { acks: done, events }, key)
316
+ .then((r) => r.status < 500, () => false)
317
+ .then((sent) => {
318
+ flushing = null;
319
+ if (!sent) {
320
+ outbox.unshift(...events);
321
+ acks.unshift(...done);
322
+ setTimeout(() => void flush(), 2000);
323
+ }
324
+ });
325
+ return flushing;
326
+ }
327
+
328
+ function write(s, message) {
329
+ if (s.proc.stdin.writable) s.proc.stdin.write(`${JSON.stringify(message)}\n`);
330
+ }
331
+
332
+ /** Claude Code for one conversation, kept open between its turns; a runner that restarted resumes it. */
333
+ function open(job) {
334
+ const args = ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", "--include-partial-messages"];
335
+ args.push("--permission-prompt-tool", "stdio", "--setting-sources", "project", "--strict-mcp-config", "--append-system-prompt", rules(known));
336
+ if (job.session.claudeSessionId) args.push("--resume", job.session.claudeSessionId);
337
+ if (known.length) args.push("--add-dir", ...known.map((r) => untilde(r.path)));
338
+ const proc = spawn("claude", args, { cwd: SPACE, stdio: ["pipe", "pipe", "pipe"], env: process.env });
339
+ const s = { id: job.session.id, proc, busy: false, turns: [], inputs: new Map(), lastAt: Date.now(), stopped: false, err: "", startedAt: 0 };
340
+ createInterface({ input: proc.stdout }).on("line", (line) => heard(s, line));
341
+ proc.stderr.on("data", (d) => (s.err = (s.err + d).slice(-2000)));
342
+ proc.on("error", (e) => {
343
+ sessions.delete(s.id);
344
+ emit({ session: s.id, type: "failed", message: `Claude Code could not start: ${e.message}` }, true);
345
+ });
346
+ proc.on("exit", (code) => {
347
+ sessions.delete(s.id);
348
+ if (s.busy) emit({ session: s.id, type: "failed", message: s.err.trim().split("\n").slice(-3).join(" ") || `Claude Code stopped (exit ${code}).` }, true);
349
+ });
350
+ sessions.set(s.id, s);
351
+ return s;
352
+ }
353
+
354
+ function ask(s, text) {
355
+ s.busy = true;
356
+ s.stopped = false;
357
+ s.startedAt = Date.now();
358
+ say(dim(` → ${text.split("\n")[0].slice(0, 70)}`));
359
+ write(s, { type: "user", message: { role: "user", content: text } });
360
+ }
361
+
362
+ /** One line of Claude's stream-json, turned into what the page shows. */
363
+ function heard(s, line) {
364
+ let m;
365
+ try {
366
+ m = JSON.parse(line);
367
+ } catch {
368
+ return;
369
+ }
370
+ s.lastAt = Date.now();
371
+ if (m.type === "system" && m.subtype === "init" && m.session_id) {
372
+ emit({ session: s.id, type: "started", claudeSessionId: m.session_id }, true);
373
+ } else if (m.type === "stream_event") {
374
+ const e = m.event;
375
+ if (e?.type === "content_block_delta" && e.delta?.type === "text_delta") emit({ session: s.id, type: "text", text: e.delta.text });
376
+ } else if (m.type === "assistant") {
377
+ const parts = m.message?.content ?? [];
378
+ for (const p of parts) if (p.type === "tool_use") emit({ session: s.id, type: "tool", id: p.id, name: p.name, summary: summarize(p.name, p.input) }, true);
379
+ const text = parts.filter((p) => p.type === "text").map((p) => p.text).join("");
380
+ if (text.trim()) emit({ session: s.id, type: "assistant", text }, true);
381
+ } else if (m.type === "user") {
382
+ for (const p of m.message?.content ?? []) if (p.type === "tool_result") emit({ session: s.id, type: "tool_result", id: p.tool_use_id, ok: !p.is_error });
383
+ } else if (m.type === "control_request" && m.request?.subtype === "can_use_tool") {
384
+ s.inputs.set(m.request_id, m.request.input);
385
+ emit({ session: s.id, type: "permission", requestId: m.request_id, tool: m.request.tool_name, summary: summarize(m.request.tool_name, m.request.input) }, true);
386
+ say(dim(` ? ${m.request.tool_name} waits for your answer on benchworks.dev`));
387
+ } else if (m.type === "result") {
388
+ s.busy = false;
389
+ emit({ session: s.id, type: "done", error: m.is_error === true && !s.stopped, stopped: s.stopped, ms: m.duration_ms ?? Date.now() - s.startedAt }, true);
390
+ say(dim(` ${s.stopped ? "stopped" : "answered"} in ${((Date.now() - s.startedAt) / 1000).toFixed(1)} s`));
391
+ const next = s.turns.shift();
392
+ if (next) ask(s, next);
393
+ }
394
+ }
395
+
396
+ /** A job from Benchworks: a message to answer, a decision on a permission, or Stop. */
397
+ function handle(job) {
398
+ let s = sessions.get(job.session.id);
399
+ if (job.kind === "turn") {
400
+ s ??= open(job);
401
+ if (s.busy) s.turns.push(job.text);
402
+ else ask(s, job.text);
403
+ } else if (job.kind === "permission") {
404
+ if (!s) return emit({ session: job.session.id, type: "done", error: true, ms: 0 }, true);
405
+ const input = s.inputs.get(job.requestId);
406
+ s.inputs.delete(job.requestId);
407
+ const response = job.behavior === "allow" ? { behavior: "allow", updatedInput: input ?? {} } : { behavior: "deny", message: "The person said no on benchworks.dev." };
408
+ write(s, { type: "control_response", response: { subtype: "success", request_id: job.requestId, response } });
409
+ } else if (job.kind === "stop") {
410
+ if (!s || !s.busy) return emit({ session: job.session.id, type: "done", stopped: true, ms: 0 }, true);
411
+ s.stopped = true;
412
+ s.turns = [];
413
+ write(s, { type: "control_request", request_id: `stop-${Date.now()}`, request: { subtype: "interrupt" } });
414
+ }
415
+ }
416
+
417
+ /** The long poll: Benchworks hands over jobs the moment the page sends them. Each is said to be had, and run once. */
418
+ async function jobs() {
419
+ await mkdir(SPACE, { recursive: true });
420
+ const seen = new Set();
421
+ for (;;) {
422
+ const r = await get("/api/runner/jobs?wait=25", key).catch(() => null);
423
+ if (!r || r.status >= 500) {
424
+ await new Promise((done) => setTimeout(done, 3000));
425
+ continue;
426
+ }
427
+ if (r.status === 401) return;
428
+ for (const job of r.body.jobs ?? []) {
429
+ acks.push(job.id);
430
+ if (seen.has(job.id)) continue;
431
+ seen.add(job.id);
432
+ handle(job);
433
+ }
434
+ if (acks.length) void flush();
435
+ }
436
+ }
437
+
438
+ // a conversation nobody spoke to for a while lets its Claude Code go; the next message resumes it
439
+ setInterval(() => {
440
+ for (const s of sessions.values()) if (!s.busy && Date.now() - s.lastAt > IDLE_MS) s.proc.kill();
441
+ }, 60_000).unref();
442
+
443
+ function quit(code = 0) {
444
+ for (const s of sessions.values()) s.proc.kill();
445
+ process.exit(code);
446
+ }
447
+
448
+ function fail(line) {
449
+ say(warn(line));
450
+ process.exitCode = 1;
451
+ }
452
+
453
+ function usage() {
454
+ say("Usage: npx benchworks connect --code XXXX-XXXX");
455
+ say(" npx benchworks connect (again, on a Mac that is connected already)");
456
+ }
457
+
458
+ /** How this Mac comes back without a code; Benchworks says which command when it pairs. */
459
+ let again = "npx benchworks connect";
460
+
461
+ process.on("SIGINT", () => {
462
+ say(`\nDisconnected. To come back, run: ${again}`);
463
+ quit(0);
464
+ });
465
+
466
+ const [command, ...rest] = process.argv.slice(2);
467
+ const at = rest.indexOf("--code");
468
+ const code = at >= 0 ? rest[at + 1] : undefined;
469
+ if (command === "connect") {
470
+ if (rest.includes("--always-on")) say(dim("--always-on comes with the next version; for now, keep this window open."));
471
+ await connect(code?.toUpperCase());
472
+ } else if (command === "--version" || command === "-v") {
473
+ say(VERSION);
474
+ } else {
475
+ usage();
476
+ if (command && command !== "--help" && command !== "-h") process.exitCode = 1;
477
+ }
478
+
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "benchworks",
3
- "version": "0.0.1",
4
- "description": "Benchworks connects your Mac to Benchworks so its agent can work on your code. Not released yet.",
3
+ "version": "0.2.0",
4
+ "description": "Connects your Mac to Benchworks, so the Assistant at benchworks.dev runs your own Claude Code with your logins and your files.",
5
+ "type": "module",
5
6
  "bin": {
6
- "benchworks": "bin.js"
7
+ "benchworks": "bin/benchworks.mjs"
7
8
  },
8
9
  "files": [
9
- "bin.js",
10
- "README.md"
10
+ "bin"
11
11
  ],
12
12
  "engines": {
13
13
  "node": ">=18"
14
14
  },
15
+ "os": [
16
+ "darwin"
17
+ ],
18
+ "homepage": "https://benchworks.dev",
15
19
  "license": "UNLICENSED"
16
20
  }
package/README.md DELETED
@@ -1,7 +0,0 @@
1
- # Benchworks
2
-
3
- Benchworks is a design workbench: review a pull request's running preview with your team, and let your own coding agent
4
- act on the feedback.
5
-
6
- This package will carry the runner that connects your Mac to Benchworks (`npx benchworks connect`). It is not released
7
- yet.
package/bin.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- console.log("The Benchworks runner is not released yet.");