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.
- package/bin/benchworks.mjs +132 -9
- package/package.json +1 -1
package/bin/benchworks.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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", "--
|
|
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
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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 === "
|
|
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