benchworks 0.3.0 → 0.4.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.
Files changed (2) hide show
  1. package/bin/benchworks.mjs +132 -9
  2. package/package.json +1 -1
@@ -5,7 +5,9 @@
5
5
  * the git repositories in the usual folders, and reports both to benchworks.dev every few seconds. When you ask the
6
6
  * Assistant something on benchworks.dev, it runs your own Claude Code here, under Benchworks' rules (not your personal
7
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.
8
+ * command waits for your Allow on the page (unless you picked Full access). Claude Code also gets Benchworks' own tools
9
+ * (`benchworks mcp`, this file again) to keep your projects on benchworks.dev, and sees the pictures you attach.
10
+ * Run it again without a code and it reconnects as the same Mac.
9
11
  */
10
12
  import { execFile, spawn } from "node:child_process";
11
13
  import { randomUUID } from "node:crypto";
@@ -15,9 +17,10 @@ import { createInterface } from "node:readline";
15
17
  import { chmod, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
16
18
  import { homedir, hostname, platform } from "node:os";
17
19
  import { join } from "node:path";
20
+ import { fileURLToPath } from "node:url";
18
21
  import { promisify } from "node:util";
19
22
 
20
- const VERSION = "0.3.0";
23
+ const VERSION = "0.4.0";
21
24
  const API = (process.env.BENCHWORKS_URL ?? "https://benchworks.dev").replace(/\/+$/, "");
22
25
  const HOME = homedir();
23
26
  // the runner's own folder: its key and the folder conversations start in (BENCHWORKS_HOME moves it, for a second pairing on one Mac)
@@ -384,12 +387,24 @@ function cleanEnv() {
384
387
  return env;
385
388
  }
386
389
 
390
+ /** Benchworks' own tools for one conversation: this file again, as an MCP server on stdio (`benchworks mcp`). */
391
+ function benchTools(spec) {
392
+ return JSON.stringify({
393
+ mcpServers: {
394
+ benchworks: { type: "stdio", command: process.execPath, args: [fileURLToPath(import.meta.url), "mcp"], env: { BENCHWORKS_SESSION: spec.id, BENCHWORKS_HOME: OWN } },
395
+ },
396
+ });
397
+ }
398
+
387
399
  function argv(spec) {
388
400
  const args = ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--permission-prompt-tool", "stdio"];
389
401
  args.push("--model", spec.model, "--effort", spec.effort);
402
+ // Full access can be picked mid-conversation; Claude Code allows the switch only to a process started able to
403
+ args.push("--allow-dangerously-skip-permissions");
390
404
  if (spec.permissionMode && spec.permissionMode !== "default") args.push("--permission-mode", spec.permissionMode);
391
405
  if (spec.cliSessionId) args.push("--resume", spec.cliSessionId);
392
- args.push("--setting-sources", "project", "--strict-mcp-config", "--append-system-prompt", rules(known));
406
+ args.push("--setting-sources", "project", "--strict-mcp-config", "--mcp-config", benchTools(spec), "--allowedTools", "mcp__benchworks");
407
+ args.push("--append-system-prompt", rules(known));
393
408
  if (known.length) args.push("--add-dir", ...known.map((r) => untilde(r.path)));
394
409
  return args;
395
410
  }
@@ -515,7 +530,10 @@ function handle(job) {
515
530
  if (!s.busy) s.startedAt = Date.now();
516
531
  s.busy = true;
517
532
  say(dim(` → ${job.text.split("\n")[0].slice(0, 70)}`));
518
- if (!write(s, { type: "user", message: { role: "user", content: [{ type: "text", text: job.text }] } })) emit({ session: spec.id, type: "refused", error: "Claude Code on this Mac stopped taking messages. Send it again." }, true);
533
+ // pictures the person attached go first, as Claude Code takes them
534
+ const pictures = (Array.isArray(job.images) ? job.images : []).map((img) => ({ type: "image", source: { type: "base64", media_type: img.mediaType, data: img.data } }));
535
+ const content = [...pictures, ...(job.text.trim() ? [{ type: "text", text: job.text }] : [])];
536
+ if (!write(s, { type: "user", message: { role: "user", content } })) emit({ session: spec.id, type: "refused", error: "Claude Code on this Mac stopped taking messages. Send it again." }, true);
519
537
  return;
520
538
  }
521
539
  case "permission": {
@@ -597,15 +615,120 @@ function usage() {
597
615
  /** How this Mac comes back without a code; Benchworks says which command when it pairs. */
598
616
  let again = "npx benchworks connect";
599
617
 
600
- process.on("SIGINT", () => {
601
- say(`\nDisconnected. To come back, run: ${again}`);
602
- quit(0);
603
- });
618
+
619
+ /* ---------------------------------------------------------------- Benchworks' tools for Claude Code (MCP on stdio) */
620
+
621
+ const TOOLS = [
622
+ {
623
+ name: "projects_list",
624
+ description: "List the projects on the person's Benchworks Projects page (the folders of their current place), with their links.",
625
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
626
+ },
627
+ {
628
+ name: "project_create",
629
+ description: "Create a project on the person's Benchworks Projects page: one product, client or idea, with optional links to its repository, pull requests and previews.",
630
+ inputSchema: {
631
+ type: "object",
632
+ properties: {
633
+ name: { type: "string", description: "The project's name, as the person would say it." },
634
+ color: { type: "string", description: "A folder tint as #rrggbb (optional)." },
635
+ links: { type: "array", items: { type: "object", properties: { label: { type: "string" }, url: { type: "string" } }, required: ["url"] } },
636
+ },
637
+ required: ["name"],
638
+ additionalProperties: false,
639
+ },
640
+ },
641
+ {
642
+ name: "project_update",
643
+ description: "Rename a project, change its tint, or replace its links (send the whole list).",
644
+ inputSchema: {
645
+ type: "object",
646
+ properties: {
647
+ id: { type: "string" },
648
+ name: { type: "string" },
649
+ color: { type: ["string", "null"] },
650
+ links: { type: "array", items: { type: "object", properties: { label: { type: "string" }, url: { type: "string" } }, required: ["url"] } },
651
+ },
652
+ required: ["id"],
653
+ additionalProperties: false,
654
+ },
655
+ },
656
+ {
657
+ name: "project_delete",
658
+ description: "Delete a project from the Projects page. Only when the person asks for it.",
659
+ inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false },
660
+ },
661
+ ];
662
+
663
+ /** One call to Benchworks for the tools, as this Mac, for the conversation the server was started for. */
664
+ async function bench(method, path, body) {
665
+ const state = await readState();
666
+ if (!state?.key) throw new Error("This Mac is not connected to Benchworks.");
667
+ const res = await fetch(`${state.url ?? API}/api/runner/bench${path}`, {
668
+ method,
669
+ headers: { "content-type": "application/json", authorization: `Bearer ${state.key}`, "x-benchworks-session": process.env.BENCHWORKS_SESSION ?? "" },
670
+ body: body === undefined ? undefined : JSON.stringify(body),
671
+ });
672
+ const answer = await res.json().catch(() => ({}));
673
+ if (!res.ok) throw new Error(answer.message ?? `Benchworks answered ${res.status}.`);
674
+ return answer;
675
+ }
676
+
677
+ async function callTool(name, args = {}) {
678
+ switch (name) {
679
+ case "projects_list":
680
+ return bench("GET", "/projects");
681
+ case "project_create":
682
+ return bench("POST", "/projects", args);
683
+ case "project_update": {
684
+ const { id, ...patch } = args;
685
+ return bench("PATCH", `/projects/${encodeURIComponent(id)}`, patch);
686
+ }
687
+ case "project_delete":
688
+ return bench("DELETE", `/projects/${encodeURIComponent(args.id)}`);
689
+ default:
690
+ throw new Error(`No tool named ${name}.`);
691
+ }
692
+ }
693
+
694
+ /** The MCP server: JSON-RPC lines on stdin and stdout, the tools above, nothing else. */
695
+ function mcp() {
696
+ const reply = (id, result) => process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}\n`);
697
+ const fault = (id, code, message) => process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } })}\n`);
698
+ createInterface({ input: process.stdin }).on("line", async (line) => {
699
+ let m;
700
+ try {
701
+ m = JSON.parse(line);
702
+ } catch {
703
+ return;
704
+ }
705
+ if (m.id === undefined) return;
706
+ if (m.method === "initialize") return reply(m.id, { protocolVersion: m.params?.protocolVersion ?? "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "benchworks", version: VERSION } });
707
+ if (m.method === "ping") return reply(m.id, {});
708
+ if (m.method === "tools/list") return reply(m.id, { tools: TOOLS });
709
+ if (m.method === "tools/call") {
710
+ try {
711
+ const result = await callTool(m.params?.name, m.params?.arguments ?? {});
712
+ return reply(m.id, { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] });
713
+ } catch (e) {
714
+ return reply(m.id, { content: [{ type: "text", text: e.message }], isError: true });
715
+ }
716
+ }
717
+ fault(m.id, -32601, `No method ${m.method}.`);
718
+ });
719
+ }
604
720
 
605
721
  const [command, ...rest] = process.argv.slice(2);
606
722
  const at = rest.indexOf("--code");
607
723
  const code = at >= 0 ? rest[at + 1] : undefined;
608
- if (command === "connect") {
724
+ if (command === "mcp") {
725
+ mcp();
726
+ } else if (command === "connect") {
727
+ // the tools server (`mcp`) speaks only JSON on stdout, so the goodbye belongs to the connected runner alone
728
+ process.on("SIGINT", () => {
729
+ say(`\nDisconnected. To come back, run: ${again}`);
730
+ quit(0);
731
+ });
609
732
  if (rest.includes("--always-on")) say(dim("--always-on comes with the next version; for now, keep this window open."));
610
733
  await connect(code?.toUpperCase());
611
734
  } else if (command === "--version" || command === "-v") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "benchworks",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
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
5
  "type": "module",
6
6
  "bin": {