agentrun-cli 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 ADDED
@@ -0,0 +1,66 @@
1
+ # agentrun CLI
2
+
3
+ Push, run, and talk to [Agentrun](https://agentrun.dev) agents from the terminal, and expose them to Claude Code, Cursor, Codex, or any MCP client.
4
+
5
+ ```
6
+ npm install -g agentrun-cli
7
+ agentrun login # paste a workspace API key from Settings, API keys
8
+ agentrun init # writes agentrun.yaml in the current directory
9
+ agentrun push # creates or updates the agent from it
10
+ agentrun run my-agent "Summarize the open incidents" --wait
11
+ ```
12
+
13
+ ## The manifest
14
+
15
+ `agentrun.yaml` at the root of a repo is the whole agent:
16
+
17
+ ```yaml
18
+ name: incident-triage
19
+ model: openrouter/qwen/qwen3-coder
20
+ timeout_minutes: 15
21
+ connectors: [pagerduty, github, slack] # accounts it acts through, connected once per workspace
22
+ instructions: |
23
+ You are the on-call triage agent. When given an alert, read the incident,
24
+ find the related deploys and errors, and post a short summary to the channel.
25
+ ```
26
+
27
+ `instructions_file: AGENTS.md` works instead of inline instructions. If the directory is a git checkout with a GitHub origin, `push` records that repo and every run clones it, so skills, scripts, and files next to the manifest travel with the agent. Set `repo:` explicitly to override.
28
+
29
+ ## Commands
30
+
31
+ | Command | What it does |
32
+ | --- | --- |
33
+ | `agentrun login [--key ar_…] [--url …]` | Stores the key in `~/.config/agentrun/config.json` (mode 600). `AGENTRUN_API_KEY` and `AGENTRUN_URL` override it. |
34
+ | `agentrun whoami` | Workspace, user, role, plan behind the key. |
35
+ | `agentrun init [--name n] [--model provider/model]` | Writes a starter manifest. |
36
+ | `agentrun push [dir]` | Create-or-update the agent named in the manifest. Idempotent. |
37
+ | `agentrun agents` | List agents with model and connectors. |
38
+ | `agentrun run <agent> <task…> [--wait] [--json]` | Start a run. `--wait` streams events and prints the result; exit code 1 on failure. |
39
+ | `agentrun logs <run-id> [--follow]` | Events of a run. |
40
+ | `agentrun send <run-id> <text…>` | Message a running or waiting run; a finished run continues as a new run with the earlier context. |
41
+ | `agentrun cancel <run-id>` | Cancel. |
42
+ | `agentrun mcp` | MCP server over stdio. |
43
+
44
+ ## MCP
45
+
46
+ Add to Claude Code:
47
+
48
+ ```
49
+ claude mcp add agentrun -- agentrun mcp
50
+ ```
51
+
52
+ Cursor, Codex, and others take the same command in their MCP config:
53
+
54
+ ```json
55
+ { "mcpServers": { "agentrun": { "command": "agentrun", "args": ["mcp"] } } }
56
+ ```
57
+
58
+ Tools: `list_agents`, `run_agent` (with `wait` to block for the result), `get_run`, `list_runs`, `send_message`, `cancel_run`, `wait_for_run`. A coding agent can hand a task to one of your Agentrun agents and read the answer back in the same session.
59
+
60
+ ## Development
61
+
62
+ ```
63
+ npm install
64
+ npm run build # tsc -> dist/
65
+ node dist/cli.js help
66
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ import { ApiError, Client, TERMINAL, follow } from "./client.js";
3
+ import { DEFAULT_URL, readConfig, requireConfig, writeConfig } from "./config.js";
4
+ import { loadManifest, writeManifest } from "./manifest.js";
5
+ const HELP = `agentrun — run agents from the terminal
6
+
7
+ agentrun login [--key ar_…] [--url https://app.agentrun.dev] store a workspace API key
8
+ agentrun whoami show the workspace this key belongs to
9
+ agentrun init [--name n] [--model provider/model] write an agentrun.yaml here
10
+ agentrun push [dir] create or update the agent from agentrun.yaml
11
+ agentrun agents list agents
12
+ agentrun run <agent> <task…> [--wait] [--json] start a run (and follow it with --wait)
13
+ agentrun logs <run-id> [--follow] print a run's events
14
+ agentrun send <run-id> <text…> message a running, waiting, or finished run
15
+ agentrun cancel <run-id>
16
+ agentrun mcp serve the MCP server over stdio
17
+
18
+ Environment: AGENTRUN_API_KEY, AGENTRUN_URL override the stored config.`;
19
+ function parse(argv) {
20
+ const [cmd = "help", ...rest] = argv;
21
+ const args = [];
22
+ const flags = {};
23
+ for (let i = 0; i < rest.length; i++) {
24
+ const a = rest[i];
25
+ if (a.startsWith("--")) {
26
+ const [k, v] = a.slice(2).split("=", 2);
27
+ if (v !== undefined)
28
+ flags[k] = v;
29
+ else if (rest[i + 1] && !rest[i + 1].startsWith("--") && ["key", "url", "name", "model"].includes(k))
30
+ flags[k] = rest[++i];
31
+ else
32
+ flags[k] = true;
33
+ }
34
+ else
35
+ args.push(a);
36
+ }
37
+ return { cmd, args, flags };
38
+ }
39
+ const ts = (ms) => new Date(ms).toISOString().slice(11, 19);
40
+ const money = (n) => `$${(n ?? 0).toFixed(3)}`;
41
+ function printEvents(events) {
42
+ for (const e of events) {
43
+ const tag = e.kind.padEnd(6);
44
+ const body = e.kind === "text" ? e.text : e.text.replace(/\n/g, "\n ");
45
+ process.stdout.write(`${ts(e.ts)} ${tag} ${body}\n`);
46
+ }
47
+ }
48
+ async function main() {
49
+ const { cmd, args, flags } = parse(process.argv.slice(2));
50
+ switch (cmd) {
51
+ case "help":
52
+ case "--help":
53
+ case "-h":
54
+ console.log(HELP);
55
+ return;
56
+ case "login": {
57
+ const url = String(flags.url ?? readConfig().url ?? DEFAULT_URL).replace(/\/$/, "");
58
+ let key = typeof flags.key === "string" ? flags.key : "";
59
+ if (!key) {
60
+ const { createInterface } = await import("node:readline/promises");
61
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
62
+ key = (await rl.question(`API key for ${url} (Settings, API keys): `)).trim();
63
+ rl.close();
64
+ }
65
+ if (!key.startsWith("ar_"))
66
+ throw new Error("That does not look like an Agentrun API key (they start with ar_).");
67
+ const me = await new Client({ url, key }).me();
68
+ const file = writeConfig({ url, key });
69
+ console.log(`Logged in to ${me.org.name} as ${me.user.email} (${me.role}). Saved to ${file}.`);
70
+ return;
71
+ }
72
+ case "whoami": {
73
+ const me = await new Client(requireConfig()).me();
74
+ console.log(`${me.org.name} (${me.org.id}) · ${me.user.email} · ${me.role} · plan ${me.org.plan}`);
75
+ return;
76
+ }
77
+ case "init": {
78
+ const name = String(flags.name ?? process.cwd().split("/").pop()?.toLowerCase().replace(/[^a-z0-9-]+/g, "-") ?? "my-agent");
79
+ const path = writeManifest(process.cwd(), { name, model: String(flags.model ?? "openrouter/qwen/qwen3-coder") });
80
+ console.log(`Wrote ${path}. Edit it, then \`agentrun push\`.`);
81
+ return;
82
+ }
83
+ case "push": {
84
+ const client = new Client(requireConfig());
85
+ const { path, manifest } = loadManifest(args[0] ?? process.cwd());
86
+ const res = await client.upsertAgent(manifest.name, { model: manifest.model, instructions: manifest.instructions, repo: manifest.repo, timeout_minutes: manifest.timeout_minutes, connectors: manifest.connectors });
87
+ console.log(`${res.created ? "Created" : "Updated"} ${manifest.name} from ${path}`);
88
+ console.log(` model ${manifest.model}`);
89
+ console.log(` repo ${manifest.repo ?? "none (runs start in an empty workspace)"}`);
90
+ console.log(` connectors ${manifest.connectors.join(", ") || "none"}`);
91
+ console.log(` ${res.url}`);
92
+ if (manifest.repo)
93
+ console.log("Runs clone the repo's default branch: commit and push agentrun.yaml so the agent and its files match.");
94
+ return;
95
+ }
96
+ case "agents": {
97
+ const agents = await new Client(requireConfig()).listAgents();
98
+ if (agents.length === 0)
99
+ return console.log("No agents. `agentrun init` then `agentrun push`.");
100
+ for (const a of agents) {
101
+ const cs = Array.isArray(a.connectors) ? a.connectors : JSON.parse(a.connectors || "[]");
102
+ console.log(`${a.name.padEnd(24)} ${a.model.padEnd(36)} ${cs.join(",") || "-"}`);
103
+ }
104
+ return;
105
+ }
106
+ case "run": {
107
+ const [agent, ...rest] = args;
108
+ const input = rest.join(" ").trim();
109
+ if (!agent || !input)
110
+ throw new Error("usage: agentrun run <agent> <task…> [--wait]");
111
+ const client = new Client(requireConfig());
112
+ const started = await client.startRun(agent, input);
113
+ if (flags.json && !flags.wait)
114
+ return console.log(JSON.stringify(started));
115
+ console.error(`run ${started.id} ${started.status}${started.deduplicated ? " (deduplicated)" : ""} · ${started.url}`);
116
+ if (!flags.wait)
117
+ return;
118
+ let final = null;
119
+ for await (const { run, events } of follow(client, started.id, { stopAtWaiting: true })) {
120
+ printEvents(events);
121
+ final = run;
122
+ }
123
+ if (final) {
124
+ console.error(`\n${final.status}${final.failure_category ? ` (${final.failure_category})` : ""} · ${final.turns} turn${final.turns === 1 ? "" : "s"} · ${final.active_seconds ?? 0}s active · ${money(final.price_usd)}`);
125
+ if (final.status === "waiting")
126
+ console.error(`waiting for a reply: agentrun send ${final.id} "<text>"`);
127
+ if (flags.json)
128
+ console.log(JSON.stringify(final));
129
+ else if (final.result)
130
+ console.log(final.result);
131
+ if (!TERMINAL.has(final.status) && final.status !== "waiting")
132
+ process.exitCode = 1;
133
+ if (["failed", "timeout", "error"].includes(final.status))
134
+ process.exitCode = 1;
135
+ }
136
+ return;
137
+ }
138
+ case "logs": {
139
+ const id = args[0];
140
+ if (!id)
141
+ throw new Error("usage: agentrun logs <run-id> [--follow]");
142
+ const client = new Client(requireConfig());
143
+ if (!flags.follow) {
144
+ const run = await client.getRun(id);
145
+ printEvents(run.events ?? []);
146
+ console.error(`${run.status} · ${run.turns} turns · ${money(run.price_usd)}`);
147
+ return;
148
+ }
149
+ for await (const { run, events } of follow(client, id)) {
150
+ printEvents(events);
151
+ if (TERMINAL.has(run.status))
152
+ console.error(`${run.status} · ${money(run.price_usd)}`);
153
+ }
154
+ return;
155
+ }
156
+ case "send": {
157
+ const [id, ...rest] = args;
158
+ const text = rest.join(" ").trim();
159
+ if (!id || !text)
160
+ throw new Error("usage: agentrun send <run-id> <text…>");
161
+ const r = await new Client(requireConfig()).sendMessage(id, text);
162
+ console.log(r.continued ? `That run had ended; continued as ${r.run_id}. ${r.url}` : `Delivered to ${r.run_id}.`);
163
+ return;
164
+ }
165
+ case "cancel": {
166
+ if (!args[0])
167
+ throw new Error("usage: agentrun cancel <run-id>");
168
+ await new Client(requireConfig()).cancelRun(args[0]);
169
+ console.log(`cancelled ${args[0]}`);
170
+ return;
171
+ }
172
+ case "mcp": {
173
+ const { serveMcp } = await import("./mcp.js");
174
+ await serveMcp();
175
+ return;
176
+ }
177
+ default:
178
+ console.error(`unknown command: ${cmd}\n`);
179
+ console.log(HELP);
180
+ process.exitCode = 2;
181
+ }
182
+ }
183
+ main().catch((e) => {
184
+ if (e instanceof ApiError && e.status === 401)
185
+ console.error("Unauthorized: the API key was rejected. Run `agentrun login` again.");
186
+ else
187
+ console.error(e instanceof Error ? e.message : String(e));
188
+ process.exitCode = 1;
189
+ });
package/dist/client.js ADDED
@@ -0,0 +1,77 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ constructor(status, message) {
4
+ super(message);
5
+ this.status = status;
6
+ }
7
+ }
8
+ export class Client {
9
+ cfg;
10
+ constructor(cfg) {
11
+ this.cfg = cfg;
12
+ }
13
+ async call(method, path, body, headers = {}) {
14
+ const res = await fetch(`${this.cfg.url}${path}`, {
15
+ method,
16
+ headers: { authorization: `Bearer ${this.cfg.key}`, "user-agent": "agentrun-cli", ...(body !== undefined ? { "content-type": "application/json" } : {}), ...headers },
17
+ body: body !== undefined ? JSON.stringify(body) : undefined,
18
+ });
19
+ const text = await res.text();
20
+ let data = text;
21
+ try {
22
+ data = JSON.parse(text);
23
+ }
24
+ catch { }
25
+ if (!res.ok) {
26
+ const msg = data?.error ?? (typeof data === "string" ? data.slice(0, 200) : res.statusText);
27
+ throw new ApiError(res.status, `${res.status} ${msg}`);
28
+ }
29
+ return data;
30
+ }
31
+ me() {
32
+ return this.call("GET", "/api/me");
33
+ }
34
+ listAgents() {
35
+ return this.call("GET", "/api/agents");
36
+ }
37
+ getAgent(name) {
38
+ return this.call("GET", `/api/agents/${encodeURIComponent(name)}`);
39
+ }
40
+ upsertAgent(name, fields) {
41
+ return this.call("PUT", `/api/agents/${encodeURIComponent(name)}`, fields);
42
+ }
43
+ startRun(agent, input, idempotencyKey) {
44
+ return this.call("POST", "/api/runs", { agent, input }, idempotencyKey ? { "idempotency-key": idempotencyKey } : {});
45
+ }
46
+ listRuns(params = {}) {
47
+ const q = new URLSearchParams(Object.entries(params).filter(([, v]) => v));
48
+ return this.call("GET", `/api/runs${q.size ? `?${q}` : ""}`);
49
+ }
50
+ getRun(id, afterSeq = -1) {
51
+ return this.call("GET", `/api/runs/${encodeURIComponent(id)}?after=${afterSeq}`);
52
+ }
53
+ sendMessage(id, text) {
54
+ return this.call("POST", `/api/runs/${encodeURIComponent(id)}/messages`, { text });
55
+ }
56
+ cancelRun(id) {
57
+ return this.call("POST", `/api/runs/${encodeURIComponent(id)}/cancel`);
58
+ }
59
+ }
60
+ export const TERMINAL = new Set(["succeeded", "failed", "timeout", "error"]);
61
+ /** Polls a run until it reaches a terminal state (or `waiting`, when `stopAtWaiting`), yielding new events as they appear. */
62
+ export async function* follow(client, id, opts = {}) {
63
+ let after = -1;
64
+ const interval = opts.intervalMs ?? 2000;
65
+ for (;;) {
66
+ const run = await client.getRun(id, after);
67
+ const events = run.events ?? [];
68
+ if (events.length)
69
+ after = events[events.length - 1].seq;
70
+ yield { run, events };
71
+ if (TERMINAL.has(run.status))
72
+ return;
73
+ if (opts.stopAtWaiting && run.status === "waiting")
74
+ return;
75
+ await new Promise((r) => setTimeout(r, interval));
76
+ }
77
+ }
package/dist/config.js ADDED
@@ -0,0 +1,28 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const DIR = process.env.AGENTRUN_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "agentrun");
5
+ const FILE = join(DIR, "config.json");
6
+ export const DEFAULT_URL = "https://app.agentrun.dev";
7
+ export function readConfig() {
8
+ let file = {};
9
+ try {
10
+ if (existsSync(FILE))
11
+ file = JSON.parse(readFileSync(FILE, "utf8"));
12
+ }
13
+ catch { }
14
+ return { url: process.env.AGENTRUN_URL ?? file.url ?? DEFAULT_URL, key: process.env.AGENTRUN_API_KEY ?? file.key };
15
+ }
16
+ export function requireConfig() {
17
+ const c = readConfig();
18
+ if (!c.key) {
19
+ console.error("Not logged in. Run `agentrun login` with a workspace API key from Settings, API keys, or set AGENTRUN_API_KEY.");
20
+ process.exit(2);
21
+ }
22
+ return { url: (c.url ?? DEFAULT_URL).replace(/\/$/, ""), key: c.key };
23
+ }
24
+ export function writeConfig(c) {
25
+ mkdirSync(DIR, { recursive: true, mode: 0o700 });
26
+ writeFileSync(FILE, JSON.stringify(c, null, 2) + "\n", { mode: 0o600 });
27
+ return FILE;
28
+ }
@@ -0,0 +1,74 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { parse, stringify } from "yaml";
4
+ export const MANIFEST_FILE = "agentrun.yaml";
5
+ /** Reads agentrun.yaml from `dir`, resolving instructions_file relative to it. */
6
+ export function loadManifest(dir) {
7
+ const path = resolve(dir, MANIFEST_FILE);
8
+ if (!existsSync(path))
9
+ throw new Error(`No ${MANIFEST_FILE} in ${resolve(dir)}. Run \`agentrun init\` to create one.`);
10
+ const doc = parse(readFileSync(path, "utf8"));
11
+ if (!doc || typeof doc !== "object")
12
+ throw new Error(`${MANIFEST_FILE} must be a mapping.`);
13
+ const name = String(doc.name ?? "").trim();
14
+ const model = String(doc.model ?? "").trim();
15
+ if (!/^[a-z0-9-]+$/.test(name))
16
+ throw new Error("name must be lowercase letters, digits, and dashes.");
17
+ if (!model.includes("/"))
18
+ throw new Error("model must be in provider/model form, e.g. openrouter/qwen/qwen3-coder.");
19
+ let instructions = typeof doc.instructions === "string" ? doc.instructions.trim() : "";
20
+ if (!instructions && typeof doc.instructions_file === "string") {
21
+ const f = resolve(dir, doc.instructions_file);
22
+ if (!existsSync(f))
23
+ throw new Error(`instructions_file ${doc.instructions_file} not found.`);
24
+ instructions = readFileSync(f, "utf8").trim();
25
+ }
26
+ if (!instructions)
27
+ throw new Error("instructions (or instructions_file) is required.");
28
+ const rawConnectors = Array.isArray(doc.connectors) ? doc.connectors : typeof doc.connectors === "string" ? doc.connectors.split(",") : [];
29
+ const connectors = [...new Set(rawConnectors.map((c) => String(c).trim().toLowerCase()).filter(Boolean))];
30
+ const timeout_minutes = Math.min(120, Math.max(1, Number.parseInt(String(doc.timeout_minutes ?? "30"), 10) || 30));
31
+ const repo = typeof doc.repo === "string" && doc.repo.trim() ? doc.repo.trim() : detectRepo(dir);
32
+ return { path, manifest: { name, model, instructions, timeout_minutes, connectors, repo } };
33
+ }
34
+ /** The GitHub URL of the origin remote, if the directory is a git checkout. Runs clone this. */
35
+ export function detectRepo(dir) {
36
+ try {
37
+ const cfg = readFileSync(join(gitDir(dir), "config"), "utf8");
38
+ const m = /\[remote "origin"\][^[]*?url\s*=\s*(\S+)/.exec(cfg);
39
+ if (!m)
40
+ return null;
41
+ const url = m[1];
42
+ const gh = /^(?:git@github\.com:|https:\/\/github\.com\/)([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url);
43
+ return gh ? `https://github.com/${gh[1]}/${gh[2]}` : null;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function gitDir(dir) {
50
+ let d = resolve(dir);
51
+ for (let i = 0; i < 20; i++) {
52
+ const g = join(d, ".git");
53
+ if (existsSync(g)) {
54
+ const st = readFileSync(g, "utf8").trim();
55
+ return st.startsWith("gitdir:") ? resolve(d, st.slice(7).trim()) : g;
56
+ }
57
+ const up = resolve(d, "..");
58
+ if (up === d)
59
+ break;
60
+ d = up;
61
+ }
62
+ throw new Error("not a git repo");
63
+ }
64
+ export function writeManifest(dir, m) {
65
+ const path = resolve(dir, MANIFEST_FILE);
66
+ if (existsSync(path))
67
+ throw new Error(`${MANIFEST_FILE} already exists here.`);
68
+ const doc = { name: m.name, model: m.model, timeout_minutes: m.timeout_minutes ?? 30 };
69
+ if (m.connectors?.length)
70
+ doc.connectors = m.connectors;
71
+ doc.instructions = m.instructions ?? "You are a helpful agent. Do exactly what the task says, using your tools. Finish with a short summary of what you did.";
72
+ writeFileSync(path, `# Agentrun agent. \`agentrun push\` creates or updates it; runs clone this repo and read this file.\n${stringify(doc, { lineWidth: 0 })}`);
73
+ return path;
74
+ }
package/dist/mcp.js ADDED
@@ -0,0 +1,78 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { Client, TERMINAL, follow } from "./client.js";
5
+ import { requireConfig } from "./config.js";
6
+ /**
7
+ * `agentrun mcp`: a stdio MCP server so Claude Code, Cursor, Codex, or another agent can start Agentrun agents,
8
+ * watch them, and talk to them. Uses the same workspace key as the CLI.
9
+ */
10
+ function summarize(run) {
11
+ const lines = [
12
+ `run ${run.id}: ${run.status}${run.failure_category ? ` (${run.failure_category})` : ""}`,
13
+ `agent ${run.agent_id} · trigger ${run.trigger} · turns ${run.turns}`,
14
+ run.active_seconds != null ? `active ${run.active_seconds}s · price $${(run.price_usd ?? 0).toFixed(3)} · model cost $${(run.model_cost_usd ?? 0).toFixed(4)}` : null,
15
+ run.result ? `\nresult:\n${run.result}` : null,
16
+ run.error ? `\nerror: ${run.error}` : null,
17
+ ];
18
+ return lines.filter(Boolean).join("\n");
19
+ }
20
+ export async function serveMcp() {
21
+ const cfg = requireConfig();
22
+ const client = new Client(cfg);
23
+ const server = new McpServer({ name: "agentrun", version: "0.1.0" });
24
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
25
+ server.registerTool("list_agents", { description: "List the agents in the Agentrun workspace: name, model, connectors, repo.", inputSchema: {} }, async () => {
26
+ const agents = await client.listAgents();
27
+ if (agents.length === 0)
28
+ return text("No agents yet. Create one with `agentrun push` or in the Agentrun UI.");
29
+ return text(agents.map((a) => `${a.name} · ${a.model} · connectors: ${(Array.isArray(a.connectors) ? a.connectors : JSON.parse(a.connectors || "[]")).join(", ") || "none"}${a.repo ? ` · repo ${a.repo}` : ""}`).join("\n"));
30
+ });
31
+ server.registerTool("run_agent", {
32
+ description: "Start an Agentrun agent with a task. Returns the run id and URL immediately; set wait=true to block until it finishes (up to wait_seconds) and get the result.",
33
+ inputSchema: { agent: z.string().describe("Agent name"), input: z.string().describe("The task"), wait: z.boolean().optional().describe("Block until the run ends"), wait_seconds: z.number().int().positive().max(1800).optional().describe("Max seconds to wait, default 600") },
34
+ }, async ({ agent, input, wait, wait_seconds }) => {
35
+ const started = await client.startRun(agent, input);
36
+ if (!wait)
37
+ return text(`started run ${started.id} (${started.status}). ${started.url}`);
38
+ const deadline = Date.now() + (wait_seconds ?? 600) * 1000;
39
+ let last = null;
40
+ for await (const { run } of follow(client, started.id, { stopAtWaiting: true })) {
41
+ last = run;
42
+ if (Date.now() > deadline)
43
+ break;
44
+ }
45
+ return text(last ? summarize(last) + (last.status === "waiting" ? "\n\nThe agent is waiting for a reply; use send_message to continue." : "") : `started run ${started.id}`);
46
+ });
47
+ server.registerTool("get_run", { description: "Get a run's status, result, cost, and recent events.", inputSchema: { run_id: z.string(), events: z.number().int().min(0).max(200).optional().describe("How many trailing events to include, default 30") } }, async ({ run_id, events }) => {
48
+ const run = await client.getRun(run_id);
49
+ const tail = (run.events ?? []).slice(-(events ?? 30)).map((e) => `[${e.kind}] ${e.text}`).join("\n");
50
+ return text(summarize(run) + (tail ? `\n\nevents:\n${tail}` : ""));
51
+ });
52
+ server.registerTool("list_runs", { description: "Recent runs, optionally filtered by agent name and status.", inputSchema: { agent: z.string().optional(), status: z.string().optional().describe("queued|running|waiting|succeeded|failed|timeout|error") } }, async ({ agent, status }) => {
53
+ let agentId;
54
+ if (agent)
55
+ agentId = (await client.getAgent(agent)).id;
56
+ const runs = await client.listRuns({ agent: agentId, status });
57
+ return text(runs.length ? runs.slice(0, 50).map((r) => `${r.id} · ${r.status} · ${new Date(r.created_at).toISOString()} · ${r.input.slice(0, 80)}`).join("\n") : "No runs.");
58
+ });
59
+ server.registerTool("send_message", { description: "Send a message to a running or waiting run (or continue a finished one in a new run with the earlier context).", inputSchema: { run_id: z.string(), text: z.string() } }, async ({ run_id, text: msg }) => {
60
+ const r = await client.sendMessage(run_id, msg);
61
+ return text(r.continued ? `The run had ended; continued as run ${r.run_id}. ${r.url}` : `Delivered to run ${r.run_id}.`);
62
+ });
63
+ server.registerTool("cancel_run", { description: "Cancel a run.", inputSchema: { run_id: z.string() } }, async ({ run_id }) => {
64
+ await client.cancelRun(run_id);
65
+ return text(`cancelled ${run_id}`);
66
+ });
67
+ server.registerTool("wait_for_run", { description: "Block until a run reaches a terminal state or starts waiting for a reply.", inputSchema: { run_id: z.string(), wait_seconds: z.number().int().positive().max(1800).optional() } }, async ({ run_id, wait_seconds }) => {
68
+ const deadline = Date.now() + (wait_seconds ?? 600) * 1000;
69
+ let last = null;
70
+ for await (const { run } of follow(client, run_id, { stopAtWaiting: true })) {
71
+ last = run;
72
+ if (Date.now() > deadline)
73
+ break;
74
+ }
75
+ return text(last ? summarize(last) + (last && !TERMINAL.has(last.status) ? "\n\n(still going)" : "") : "no such run");
76
+ });
77
+ await server.connect(new StdioServerTransport());
78
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "agentrun-cli",
3
+ "version": "0.1.0",
4
+ "description": "Push, run, and talk to Agentrun agents from the terminal, plus an MCP server for Claude Code, Cursor, and friends.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "agentrun": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p .",
19
+ "dev": "node --experimental-strip-types src/cli.ts",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.20.0",
24
+ "yaml": "^2.9.1",
25
+ "zod": "^3.25.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.0.0",
29
+ "typescript": "^5.9.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/stirredo/agentrun.git",
34
+ "directory": "cli"
35
+ },
36
+ "homepage": "https://agentrun.dev",
37
+ "keywords": [
38
+ "agents",
39
+ "ai",
40
+ "cli",
41
+ "mcp",
42
+ "opencode",
43
+ "automation"
44
+ ]
45
+ }