@camelai/camelrun 0.0.0 → 0.11.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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/api.js +73 -0
- package/dist/bin.js +3 -0
- package/dist/cli.js +363 -0
- package/dist/config.js +42 -0
- package/dist/manifest-files.js +21 -0
- package/dist/manifest.js +110 -0
- package/dist/mcp.js +41 -0
- package/dist/ops.js +151 -0
- package/dist/tools.js +141 -0
- package/dist/version.js +2 -0
- package/package.json +37 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qaml-ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @camelai/camelrun
|
|
2
|
+
|
|
3
|
+
The camelRun CLI: deploy and manage agents on [camelRun](https://agents.camelai.dev)
|
|
4
|
+
from a terminal, a script or CI, and give a coding agent the same powers over MCP.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install -g @camelai/camelrun
|
|
8
|
+
camelrun login # an API key from the console's API tokens page
|
|
9
|
+
camelrun init support # writes agent.yaml
|
|
10
|
+
camelrun deploy # the definition, and the agents it lists
|
|
11
|
+
camelrun run support "Which tickets look urgent?"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`agent.yaml` keeps an agent's configuration in your repository; deploying again makes a new
|
|
15
|
+
revision only when it changed, and `--apply` moves live agents to it between their turns:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
key: support
|
|
19
|
+
model: anthropic/claude-sonnet-5-5
|
|
20
|
+
systemPromptFile: prompts/support.md
|
|
21
|
+
builtins: [web_search, ask_user]
|
|
22
|
+
mcpServers:
|
|
23
|
+
- name: app
|
|
24
|
+
url: https://app.example.com/mcp
|
|
25
|
+
auth: { type: bearer, token: "${APP_MCP_TOKEN}" }
|
|
26
|
+
agents:
|
|
27
|
+
- key: support
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Output is JSON when stdout is not a terminal (or with `--json`); exit code 2 means a run waits
|
|
31
|
+
on a person (`camelrun inputs`, `camelrun answer`).
|
|
32
|
+
|
|
33
|
+
## For coding agents
|
|
34
|
+
|
|
35
|
+
The hosted MCP server needs nothing installed; sign in when the client asks:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
claude mcp add --transport http camelrun https://agents.camelai.dev/mcp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or run it locally, where its `deploy` also reads your repository's manifests:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
claude mcp add camelrun -- npx -y @camelai/camelrun mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Every command, the manifest's fields and the MCP tools:
|
|
48
|
+
[CLI and MCP server](https://agents.camelai.dev/docs/reference/cli.md).
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** A refused call: the runtime's `error` message, with the HTTP status and its `code` when it gave one. */
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
constructor(status, message, code) { super(message); this.status = status; this.code = code; }
|
|
6
|
+
}
|
|
7
|
+
/** SHA-256 as hex, with Web Crypto: the same in Node and browsers. */
|
|
8
|
+
async function sha256(text) {
|
|
9
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
10
|
+
return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, "0")).join("");
|
|
11
|
+
}
|
|
12
|
+
/** The runtime's REST API, called with a tenant API key (`art_…`). */
|
|
13
|
+
export class Api {
|
|
14
|
+
url;
|
|
15
|
+
apiKey;
|
|
16
|
+
headers;
|
|
17
|
+
fetch;
|
|
18
|
+
tenant;
|
|
19
|
+
// Browser fetch requires the global receiver, even when stored on this client.
|
|
20
|
+
constructor(credentials, fetch = globalThis.fetch.bind(globalThis)) {
|
|
21
|
+
this.url = credentials.url.replace(/\/+$/, "");
|
|
22
|
+
this.apiKey = credentials.apiKey;
|
|
23
|
+
this.headers = credentials.headers ?? {};
|
|
24
|
+
this.fetch = fetch;
|
|
25
|
+
}
|
|
26
|
+
async call(method, path, body, headers = {}) {
|
|
27
|
+
const response = await this.fetch(this.url + path, {
|
|
28
|
+
method,
|
|
29
|
+
headers: { ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}), ...this.headers, ...(body !== undefined ? { "Content-Type": "application/json" } : {}), ...headers },
|
|
30
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
const text = await response.text();
|
|
33
|
+
let json;
|
|
34
|
+
try {
|
|
35
|
+
json = text ? JSON.parse(text) : undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
json = undefined;
|
|
39
|
+
}
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const message = json?.error ?? json?.message ?? (text.slice(0, 300) || response.statusText);
|
|
42
|
+
throw new ApiError(response.status, response.status === 401 ? `${message} (check your API key: camelrun login)` : message, json?.code);
|
|
43
|
+
}
|
|
44
|
+
return json;
|
|
45
|
+
}
|
|
46
|
+
get(path) { return this.call("GET", path); }
|
|
47
|
+
/** A public document of the runtime's (its docs), as text. */
|
|
48
|
+
async text(path) {
|
|
49
|
+
const response = await this.fetch(this.url + path);
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
throw new ApiError(response.status, `${path}: ${response.status === 404 ? "no such page (read_docs with no path lists them)" : response.statusText}`);
|
|
52
|
+
return response.text();
|
|
53
|
+
}
|
|
54
|
+
me() { return this.get("/v1/me"); }
|
|
55
|
+
/** A definition's id from its key: the runtime derives it from the tenant and the key. */
|
|
56
|
+
async definitionId(keyOrId) {
|
|
57
|
+
if (/^def_[a-f0-9]{20}$/.test(keyOrId))
|
|
58
|
+
return keyOrId;
|
|
59
|
+
this.tenant ??= this.me().then(me => me.tenant);
|
|
60
|
+
return `def_${(await sha256(`${await this.tenant}:${keyOrId}`)).slice(0, 20)}`;
|
|
61
|
+
}
|
|
62
|
+
/** An agent's id from its key or id (`client_…`). */
|
|
63
|
+
async agentId(keyOrId) {
|
|
64
|
+
if (keyOrId.startsWith("client_"))
|
|
65
|
+
return keyOrId;
|
|
66
|
+
const agents = await this.get("/v1/agents");
|
|
67
|
+
const found = agents.find(agent => agent.key === keyOrId);
|
|
68
|
+
if (!found)
|
|
69
|
+
throw new ApiError(404, `No agent with key ${JSON.stringify(keyOrId)} (camelrun agents list shows them)`);
|
|
70
|
+
return found.id;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export const enc = encodeURIComponent;
|
package/dist/bin.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { Api, enc } from "./api.js";
|
|
5
|
+
import { DEFAULT_URL, forget, resolve, save } from "./config.js";
|
|
6
|
+
import { DEFAULT_FILES, template } from "./manifest.js";
|
|
7
|
+
import { findManifest, loadManifests } from "./manifest-files.js";
|
|
8
|
+
import * as ops from "./ops.js";
|
|
9
|
+
import { VERSION } from "./version.js";
|
|
10
|
+
const HELP = `camelrun: deploy and manage agents on camelRun
|
|
11
|
+
|
|
12
|
+
Usage: camelrun <command> [options]
|
|
13
|
+
|
|
14
|
+
Setup
|
|
15
|
+
login [--api-key art_…] Save an API key (checked against the runtime)
|
|
16
|
+
logout Forget the saved key
|
|
17
|
+
whoami The tenant the key belongs to, and its default model
|
|
18
|
+
models [--available] Models, or only those your account can use
|
|
19
|
+
init [key] [--model m] Write an agent.yaml manifest here
|
|
20
|
+
|
|
21
|
+
Deploy
|
|
22
|
+
deploy [file…] [--apply] [--dry-run]
|
|
23
|
+
Upsert each manifest's definition by its key and make its agents;
|
|
24
|
+
--apply also moves live agents to the new revision between turns
|
|
25
|
+
|
|
26
|
+
Agents (an agent is its key or its id, client_…)
|
|
27
|
+
agents list
|
|
28
|
+
agents get <agent> Configuration, definition revision and tool sources
|
|
29
|
+
agents create <key> [--definition key|id] [--model m] [--prompt text] [--name n]
|
|
30
|
+
agents configure <agent> [--model m] [--prompt text] [--prompt-append text] [--thinking level]
|
|
31
|
+
agents delete <agent> --yes Stop it and purge its history and files
|
|
32
|
+
run <agent> <message…> [--wait s] [--no-wait] [--from user] [--steer] [--allow-disconnected]
|
|
33
|
+
runs get <agent> <requestId> [--wait s]
|
|
34
|
+
history <agent> [--limit n]
|
|
35
|
+
abort <agent>
|
|
36
|
+
inputs [agent] Questions and approvals waiting on someone
|
|
37
|
+
answer <agent> <inputId> <value> true/false, text, JSON, or "decline"
|
|
38
|
+
schedules list <agent>
|
|
39
|
+
schedules add <agent> --text t (--in s | --at iso) [--every s]
|
|
40
|
+
schedules delete <agent> <scheduleId>
|
|
41
|
+
|
|
42
|
+
Definitions (a definition is its key or its id, def_…)
|
|
43
|
+
definitions list
|
|
44
|
+
definitions get <definition>
|
|
45
|
+
definitions agents <definition> Its agents and the revision each has
|
|
46
|
+
definitions delete <definition> --yes
|
|
47
|
+
|
|
48
|
+
MCP
|
|
49
|
+
mcp Serve these commands as an MCP server over stdio
|
|
50
|
+
(or connect clients to the hosted one: https://agents.camelai.dev/mcp)
|
|
51
|
+
|
|
52
|
+
Options
|
|
53
|
+
--json Print JSON (the default when stdout is not a terminal)
|
|
54
|
+
--api-key <key>, --url <url> Instead of CAMELAI_API_KEY / CAMELAI_URL / the saved login
|
|
55
|
+
|
|
56
|
+
Docs: https://agents.camelai.dev/llms.txt`;
|
|
57
|
+
const OPTIONS = {
|
|
58
|
+
json: { type: "boolean" }, help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" },
|
|
59
|
+
"api-key": { type: "string" }, url: { type: "string" },
|
|
60
|
+
apply: { type: "boolean" }, "dry-run": { type: "boolean" }, available: { type: "boolean" }, force: { type: "boolean" }, yes: { type: "boolean", short: "y" },
|
|
61
|
+
model: { type: "string" }, prompt: { type: "string" }, "prompt-append": { type: "string" }, thinking: { type: "string" }, name: { type: "string" }, definition: { type: "string" },
|
|
62
|
+
wait: { type: "string" }, "no-wait": { type: "boolean" }, from: { type: "string" }, steer: { type: "boolean" }, "allow-disconnected": { type: "boolean" }, "request-id": { type: "string" },
|
|
63
|
+
limit: { type: "string" }, text: { type: "string" }, in: { type: "string" }, at: { type: "string" }, every: { type: "string" },
|
|
64
|
+
};
|
|
65
|
+
class UsageError extends Error {
|
|
66
|
+
}
|
|
67
|
+
/** Run the CLI; resolves with its exit code: 0, 1 for an error, 2 when a run waits on a person. */
|
|
68
|
+
export async function main(argv, io = { out: text => process.stdout.write(text + "\n"), err: text => process.stderr.write(text + "\n"), tty: !!process.stdout.isTTY, env: process.env, cwd: process.cwd() }) {
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
io.err(`${error.message}\n\nRun camelrun --help for usage.`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
const { values: flags, positionals } = parsed;
|
|
78
|
+
if (flags.version) {
|
|
79
|
+
io.out(VERSION);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
const [command, ...args] = positionals;
|
|
83
|
+
if (!command || flags.help || command === "help") {
|
|
84
|
+
io.out(HELP);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
const json = flags.json || !io.tty;
|
|
88
|
+
const print = (value, human) => io.out(json || !human ? JSON.stringify(value, null, 2) : human());
|
|
89
|
+
const api = () => new Api(resolve({ apiKey: flags["api-key"], url: flags.url }, io.env));
|
|
90
|
+
try {
|
|
91
|
+
switch (command) {
|
|
92
|
+
case "mcp": {
|
|
93
|
+
const { serve } = await import("./mcp.js");
|
|
94
|
+
await serve(() => api(), { cwd: io.cwd });
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
case "login": return await login(flags, io, print);
|
|
98
|
+
case "logout": {
|
|
99
|
+
const path = forget(io.env);
|
|
100
|
+
print({ loggedOut: !!path }, () => path ? `Removed ${path}` : "No saved key");
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
case "whoami": {
|
|
104
|
+
const client = api();
|
|
105
|
+
const me = await client.me();
|
|
106
|
+
print({ ...me, url: client.url }, () => `${me.login ?? me.tenant} (tenant ${me.tenant}) on ${client.url}\nDefault model: ${me.defaultModel}`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
case "models": {
|
|
110
|
+
const models = await api().get(`/v1/models${flags.available ? "?available=true" : ""}`);
|
|
111
|
+
print(models.map(({ id, name, available, contextWindow, reasoning, cost }) => ({ id, name, available, contextWindow, reasoning, cost })), () => table(models, ["id", "available", "contextWindow", "reasoning"]));
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
case "init": {
|
|
115
|
+
const key = args[0] ?? io.cwd.split("/").pop().replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 80);
|
|
116
|
+
const path = `${io.cwd}/agent.yaml`;
|
|
117
|
+
if (DEFAULT_FILES.some(name => existsSync(`${io.cwd}/${name}`)) && !flags.force)
|
|
118
|
+
throw new UsageError("A manifest is already here (--force overwrites agent.yaml)");
|
|
119
|
+
writeFileSync(path, template(key, flags.model));
|
|
120
|
+
print({ file: path, key }, () => `Wrote ${path}. Edit it, then: camelrun deploy`);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
case "deploy": {
|
|
124
|
+
const files = args.length ? args : [findManifest(io.cwd)];
|
|
125
|
+
const manifests = files.flatMap(file => loadManifests(file.startsWith("/") ? file : `${io.cwd}/${file}`, io.env));
|
|
126
|
+
const results = await ops.deploy(api(), manifests, { apply: flags.apply, dryRun: flags["dry-run"] });
|
|
127
|
+
if (flags["dry-run"] && json)
|
|
128
|
+
print(results.map((result, index) => ({ ...result, definition: ops.redact(manifests[index].definition) })));
|
|
129
|
+
else
|
|
130
|
+
print(results, () => results.map(describeDeploy).join("\n"));
|
|
131
|
+
return results.some(result => result.agents.some(agent => agent.status === "failed") || result.applied?.some(applied => applied.status === "failed")) ? 1 : 0;
|
|
132
|
+
}
|
|
133
|
+
case "agents": return await agents(args, flags, api, print);
|
|
134
|
+
case "definitions": return await definitions(args, flags, api, print);
|
|
135
|
+
case "run": {
|
|
136
|
+
const [agent, ...words] = args;
|
|
137
|
+
if (!agent || !words.length)
|
|
138
|
+
throw new UsageError("Usage: camelrun run <agent> <message…>");
|
|
139
|
+
const result = await ops.run(api(), agent, words.join(" "), { wait: waitSeconds(flags), from: flags.from, steer: flags.steer, allowDisconnected: flags["allow-disconnected"], requestId: flags["request-id"] });
|
|
140
|
+
return printRun(result, print, io);
|
|
141
|
+
}
|
|
142
|
+
case "runs": {
|
|
143
|
+
const [sub, agent, requestId] = args;
|
|
144
|
+
if (sub !== "get" || !agent || !requestId)
|
|
145
|
+
throw new UsageError("Usage: camelrun runs get <agent> <requestId> [--wait s]");
|
|
146
|
+
const client = api();
|
|
147
|
+
return printRun(await ops.waitFor(client, await client.agentId(agent), requestId, flags.wait ? Number(flags.wait) : 0), print, io);
|
|
148
|
+
}
|
|
149
|
+
case "history": {
|
|
150
|
+
if (!args[0])
|
|
151
|
+
throw new UsageError("Usage: camelrun history <agent> [--limit n]");
|
|
152
|
+
const page = await ops.history(api(), args[0], flags.limit ? Number(flags.limit) : 20);
|
|
153
|
+
print(page, () => page.messages.map((message) => {
|
|
154
|
+
const head = message.role === "toolResult" ? `[${message.tool} →]` : `${message.role}${"from" in message ? ` (${message.from})` : ""}:`;
|
|
155
|
+
const calls = "toolCalls" in message ? message.toolCalls.map(call => `\n → ${call.name}(${JSON.stringify(call.arguments).slice(0, 200)})`).join("") : "";
|
|
156
|
+
return `${head} ${message.text ?? ""}${calls}`;
|
|
157
|
+
}).join("\n\n") || "(no messages)");
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
case "abort": {
|
|
161
|
+
if (!args[0])
|
|
162
|
+
throw new UsageError("Usage: camelrun abort <agent>");
|
|
163
|
+
const client = api();
|
|
164
|
+
const id = await client.agentId(args[0]);
|
|
165
|
+
print(await client.call("POST", `/v1/agents/${enc(id)}/abort`, {}), () => `Aborted the running turn of ${args[0]}`);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
case "inputs": {
|
|
169
|
+
const client = api();
|
|
170
|
+
const inputs = args[0] ? await client.get(`/v1/agents/${enc(await client.agentId(args[0]))}/inputs?state=pending`) : await client.get("/v1/inputs?state=pending");
|
|
171
|
+
print(inputs, () => inputs.length ? inputs.map(input => `${input.id} ${input.agent} ${input.kind}: ${input.message ?? JSON.stringify(input.detail).slice(0, 200)}`).join("\n") : "Nothing is waiting");
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
case "answer": {
|
|
175
|
+
const [agent, inputId, ...rest] = args;
|
|
176
|
+
if (!agent || !inputId || !rest.length)
|
|
177
|
+
throw new UsageError("Usage: camelrun answer <agent> <inputId> <value>");
|
|
178
|
+
const raw = rest.join(" ");
|
|
179
|
+
let value = raw;
|
|
180
|
+
try {
|
|
181
|
+
value = JSON.parse(raw);
|
|
182
|
+
}
|
|
183
|
+
catch { /* text */ }
|
|
184
|
+
const result = await ops.answer(api(), agent, inputId, value, { from: flags.from, wait: waitSeconds(flags) });
|
|
185
|
+
return "requestId" in result ? printRun(result, print, io) : (print(result, () => result.note), 0);
|
|
186
|
+
}
|
|
187
|
+
case "schedules": return await schedules(args, flags, api, print);
|
|
188
|
+
default: throw new UsageError(`Unknown command: ${command}. Run camelrun --help`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
io.err(json && !(error instanceof UsageError) ? JSON.stringify({ error: error.message, ...("status" in error ? { status: error.status, code: error.code } : {}) }) : `Error: ${error.message}`);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function login(flags, io, print) {
|
|
197
|
+
let apiKey = flags["api-key"] || io.env.CAMELAI_API_KEY;
|
|
198
|
+
if (!apiKey) {
|
|
199
|
+
if (!process.stdin.isTTY)
|
|
200
|
+
throw new UsageError("Pass --api-key art_… (or set CAMELAI_API_KEY); create one under API tokens at https://agents.camelai.dev/console");
|
|
201
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
202
|
+
apiKey = (await prompt.question("API key (from https://agents.camelai.dev/console, API tokens): ")).trim();
|
|
203
|
+
prompt.close();
|
|
204
|
+
}
|
|
205
|
+
const url = flags.url || io.env.CAMELAI_URL || DEFAULT_URL;
|
|
206
|
+
const me = await new Api({ apiKey, url }).me();
|
|
207
|
+
const path = save({ apiKey, url }, io.env);
|
|
208
|
+
print({ tenant: me.tenant, url, saved: path }, () => `Logged in as ${me.login ?? me.tenant} on ${url} (key saved to ${path})`);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
async function agents(args, flags, api, print) {
|
|
212
|
+
const [sub, target] = args;
|
|
213
|
+
const client = api();
|
|
214
|
+
switch (sub) {
|
|
215
|
+
case "list":
|
|
216
|
+
case undefined: {
|
|
217
|
+
const list = await client.get("/v1/agents");
|
|
218
|
+
print(list, () => list.length ? table(list, ["key", "id", "model", "running", "connected"]) : "No agents yet: camelrun deploy, or camelrun agents create <key>");
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
case "get": {
|
|
222
|
+
if (!target)
|
|
223
|
+
throw new UsageError("Usage: camelrun agents get <agent>");
|
|
224
|
+
const detail = await client.get(`/v1/agents/${enc(await client.agentId(target))}`);
|
|
225
|
+
print(detail);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
case "create": {
|
|
229
|
+
if (!target)
|
|
230
|
+
throw new UsageError("Usage: camelrun agents create <key> [--definition d] [--model m] [--prompt text]");
|
|
231
|
+
const created = await ops.upsertAgent(client, target, { definition: flags.definition, model: flags.model, systemPrompt: flags.prompt, systemPromptAppend: flags["prompt-append"], name: flags.name });
|
|
232
|
+
print(created, () => `${created.key}: ${created.id}${created.reconfigured ? " (reconfigured)" : ""}`);
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
case "configure": {
|
|
236
|
+
if (!target)
|
|
237
|
+
throw new UsageError("Usage: camelrun agents configure <agent> [--model m] [--prompt text] [--prompt-append text] [--thinking level]");
|
|
238
|
+
const changes = Object.fromEntries(Object.entries({ model: flags.model, systemPrompt: flags.prompt, systemPromptAppend: flags["prompt-append"], thinkingLevel: flags.thinking }).filter(([, value]) => value !== undefined));
|
|
239
|
+
if (!Object.keys(changes).length)
|
|
240
|
+
throw new UsageError("Nothing to change: pass --model, --prompt, --prompt-append or --thinking");
|
|
241
|
+
const result = await ops.configure(client, target, changes);
|
|
242
|
+
print(result, () => result.status === "failed" ? `Failed: ${result.error?.message}` : result.status === "running" ? `Queued behind its current run (request ${result.requestId})` : `Configured ${target}`);
|
|
243
|
+
return result.status === "failed" ? 1 : 0;
|
|
244
|
+
}
|
|
245
|
+
case "delete": {
|
|
246
|
+
if (!target)
|
|
247
|
+
throw new UsageError("Usage: camelrun agents delete <agent> --yes");
|
|
248
|
+
if (!flags.yes)
|
|
249
|
+
throw new UsageError(`Deleting ${target} stops it and purges its history and files. Pass --yes to go ahead.`);
|
|
250
|
+
const id = await client.agentId(target);
|
|
251
|
+
print(await client.call("DELETE", `/v1/agents/${enc(id)}`), () => `Deleted ${target} (${id})`);
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
default: throw new UsageError(`Unknown: agents ${sub}. Run camelrun --help`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function definitions(args, flags, api, print) {
|
|
258
|
+
const [sub, target] = args;
|
|
259
|
+
const client = api();
|
|
260
|
+
switch (sub) {
|
|
261
|
+
case "list":
|
|
262
|
+
case undefined: {
|
|
263
|
+
const list = await client.get("/v1/definitions");
|
|
264
|
+
print(list, () => list.length ? table(list, ["id", "name", "revision", "model"]) : "No definitions yet: camelrun init, then camelrun deploy");
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
case "get":
|
|
268
|
+
case "agents":
|
|
269
|
+
case "delete": {
|
|
270
|
+
if (!target)
|
|
271
|
+
throw new UsageError(`Usage: camelrun definitions ${sub} <definition>`);
|
|
272
|
+
const id = await client.definitionId(target);
|
|
273
|
+
if (sub === "get")
|
|
274
|
+
print(await client.get(`/v1/definitions/${id}`));
|
|
275
|
+
else if (sub === "agents") {
|
|
276
|
+
const list = await client.get(`/v1/definitions/${id}/agents`);
|
|
277
|
+
print(list, () => list.length ? table(list, Object.keys(list[0]).slice(0, 5)) : "No agents");
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
if (!flags.yes)
|
|
281
|
+
throw new UsageError(`Pass --yes to delete ${target}; its agents are left as they are`);
|
|
282
|
+
print(await client.call("DELETE", `/v1/definitions/${id}`), () => `Deleted ${target} (${id})`);
|
|
283
|
+
}
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
default: throw new UsageError(`Unknown: definitions ${sub}. Run camelrun --help`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function schedules(args, flags, api, print) {
|
|
290
|
+
const [sub, agent, scheduleId] = args;
|
|
291
|
+
if (!agent)
|
|
292
|
+
throw new UsageError("Usage: camelrun schedules list|add|delete <agent> …");
|
|
293
|
+
const client = api();
|
|
294
|
+
const id = await client.agentId(agent);
|
|
295
|
+
switch (sub) {
|
|
296
|
+
case "list": {
|
|
297
|
+
const list = await client.get(`/v1/agents/${enc(id)}/schedules`);
|
|
298
|
+
print(list, () => list.length ? list.map(item => `${item.id} ${new Date(item.dueAt).toISOString()}${item.everySeconds ? ` every ${item.everySeconds}s` : ""} ${item.text ?? item.code}`).join("\n") : "No schedules");
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
case "add": {
|
|
302
|
+
if (!flags.text || (!flags.in && !flags.at))
|
|
303
|
+
throw new UsageError("Usage: camelrun schedules add <agent> --text t (--in seconds | --at iso) [--every seconds]");
|
|
304
|
+
const body = { text: flags.text, ...(flags.in ? { inSeconds: Number(flags.in) } : { at: flags.at }), ...(flags.every ? { everySeconds: Number(flags.every) } : {}) };
|
|
305
|
+
const created = await client.call("POST", `/v1/agents/${enc(id)}/schedules`, body);
|
|
306
|
+
print(created, () => `${created.id}: due ${new Date(created.dueAt).toISOString()}`);
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
case "delete": {
|
|
310
|
+
if (!scheduleId)
|
|
311
|
+
throw new UsageError("Usage: camelrun schedules delete <agent> <scheduleId>");
|
|
312
|
+
print(await client.call("DELETE", `/v1/agents/${enc(id)}/schedules/${enc(scheduleId)}`), () => `Deleted ${scheduleId}`);
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
default: throw new UsageError(`Unknown: schedules ${sub}. Run camelrun --help`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function waitSeconds(flags) {
|
|
319
|
+
if (flags["no-wait"])
|
|
320
|
+
return 0;
|
|
321
|
+
if (flags.wait === undefined)
|
|
322
|
+
return undefined;
|
|
323
|
+
const seconds = Number(flags.wait);
|
|
324
|
+
if (!Number.isFinite(seconds) || seconds < 0)
|
|
325
|
+
throw new UsageError("--wait is a number of seconds");
|
|
326
|
+
return seconds;
|
|
327
|
+
}
|
|
328
|
+
function printRun(result, print, io) {
|
|
329
|
+
print(result, () => {
|
|
330
|
+
switch (result.status) {
|
|
331
|
+
case "running": return `Still running: camelrun runs get ${result.agent} ${result.requestId} --wait 60`;
|
|
332
|
+
case "failed": return `Failed (${result.error?.code}): ${result.error?.message}`;
|
|
333
|
+
case "input_required": return [result.text, ...(result.inputs ?? []).map(input => `Waiting on ${input.kind} ${input.id}: ${input.message ?? JSON.stringify(input.detail)}\n camelrun answer ${result.agent} ${input.id} <value>`)].filter(Boolean).join("\n");
|
|
334
|
+
default: return [result.text || "(no reply)", ...(result.toolErrors ?? []).map(error => `tool error: ${JSON.stringify(error)}`)].join("\n");
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
if (result.status === "failed")
|
|
338
|
+
return 1;
|
|
339
|
+
if (result.status === "input_required") {
|
|
340
|
+
if (io.tty)
|
|
341
|
+
io.err("(the run waits on the input above)");
|
|
342
|
+
return 2;
|
|
343
|
+
}
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
function table(rows, columns) {
|
|
347
|
+
const cell = (value) => value === null || value === undefined ? "-" : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
348
|
+
const widths = columns.map(column => Math.max(column.length, ...rows.map(row => cell(row[column]).length)));
|
|
349
|
+
const line = (values) => values.map((value, index) => value.padEnd(widths[index])).join(" ").trimEnd();
|
|
350
|
+
return [line(columns), ...rows.map(row => line(columns.map(column => cell(row[column]))))].join("\n");
|
|
351
|
+
}
|
|
352
|
+
function describeDeploy(result) {
|
|
353
|
+
const lines = [`${result.key}: ${result.status}${result.status === "planned" ? ` (now at revision ${result.revision || "none"})` : ` (${result.id}, revision ${result.revision})`}`];
|
|
354
|
+
for (const source of (result.toolSources ?? []))
|
|
355
|
+
lines.push(` tools from ${source.name}: ${source.status === "error" ? `error: ${source.error}` : `${source.tools?.length ?? 0} tools`}`);
|
|
356
|
+
for (const applied of result.applied ?? [])
|
|
357
|
+
lines.push(` applied to ${applied.agent}: ${applied.status}${applied.error ? ` (${applied.error})` : ""}`);
|
|
358
|
+
if (result.status === "updated" && !result.applied)
|
|
359
|
+
lines.push(" live agents keep their revision; deploy with --apply to move them");
|
|
360
|
+
for (const agent of result.agents)
|
|
361
|
+
lines.push(` agent ${agent.key}: ${agent.status}${agent.id ? ` (${agent.id})` : ""}${agent.reconfigured ? ", reconfigured between its turns" : ""}${agent.error ? `: ${agent.error}` : ""}`);
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export const DEFAULT_URL = "https://agents.camelai.dev";
|
|
5
|
+
/** Where `camelrun login` keeps the key: $CAMELRUN_CONFIG, else $XDG_CONFIG_HOME/camelrun, else ~/.config/camelrun. */
|
|
6
|
+
export function configPath(env = process.env) {
|
|
7
|
+
if (env.CAMELRUN_CONFIG)
|
|
8
|
+
return env.CAMELRUN_CONFIG;
|
|
9
|
+
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "camelrun", "credentials.json");
|
|
10
|
+
}
|
|
11
|
+
export function readSaved(env = process.env) {
|
|
12
|
+
const path = configPath(env);
|
|
13
|
+
if (!existsSync(path))
|
|
14
|
+
return {};
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function save(credentials, env = process.env) {
|
|
23
|
+
const path = configPath(env);
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
25
|
+
writeFileSync(path, JSON.stringify(credentials, null, 2) + "\n", { mode: 0o600 });
|
|
26
|
+
chmodSync(path, 0o600);
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
export function forget(env = process.env) {
|
|
30
|
+
const path = configPath(env);
|
|
31
|
+
const had = existsSync(path);
|
|
32
|
+
rmSync(path, { force: true });
|
|
33
|
+
return had ? path : undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Flags first, then CAMELAI_API_KEY / CAMELAI_URL, then what `camelrun login` saved. */
|
|
36
|
+
export function resolve(flags, env = process.env) {
|
|
37
|
+
const saved = readSaved(env);
|
|
38
|
+
const apiKey = flags.apiKey || env.CAMELAI_API_KEY || saved.apiKey;
|
|
39
|
+
if (!apiKey)
|
|
40
|
+
throw new Error("No API key: run `camelrun login`, or set CAMELAI_API_KEY (create one under API tokens at https://agents.camelai.dev/console)");
|
|
41
|
+
return { apiKey, url: flags.url || env.CAMELAI_URL || saved.url || DEFAULT_URL };
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, resolve } from "node:path";
|
|
3
|
+
import { DEFAULT_FILES, fromDocuments, parseManifests } from "./manifest.js";
|
|
4
|
+
/** The manifest in `cwd`: agent.yaml, agent.yml or agent.json. */
|
|
5
|
+
export function findManifest(cwd = process.cwd()) {
|
|
6
|
+
const found = DEFAULT_FILES.map(name => resolve(cwd, name)).find(path => existsSync(path));
|
|
7
|
+
if (!found)
|
|
8
|
+
throw new Error(`No manifest: expected one of ${DEFAULT_FILES.join(", ")} here, or pass a path (camelrun init writes one)`);
|
|
9
|
+
return found;
|
|
10
|
+
}
|
|
11
|
+
/** Files a manifest at `path` names, read relative to it. */
|
|
12
|
+
export const filesBeside = (path) => (relative) => readFileSync(resolve(dirname(path), relative), "utf8");
|
|
13
|
+
/** The manifests in a file, with `${NAME}` read from `env` and the files they name read beside it. */
|
|
14
|
+
export function loadManifests(file, env = process.env) {
|
|
15
|
+
const path = resolve(file);
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
throw new Error(`${file}: no such file`);
|
|
18
|
+
const text = readFileSync(path, "utf8");
|
|
19
|
+
const source = { env, readFile: filesBeside(path) };
|
|
20
|
+
return extname(path) === ".json" ? fromDocuments([JSON.parse(text)], path, file, source) : parseManifests(text, file, source).map(manifest => ({ ...manifest, file: path }));
|
|
21
|
+
}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { parseAllDocuments, parse as parseYaml } from "yaml";
|
|
2
|
+
export const DEFAULT_FILES = ["agent.yaml", "agent.yml", "agent.json"];
|
|
3
|
+
const KEY = /^[A-Za-z0-9_-]{1,80}$/;
|
|
4
|
+
/** Manifests from YAML text (several documents are several manifests). */
|
|
5
|
+
export function parseManifests(text, where, source = {}) {
|
|
6
|
+
const documents = parseAllDocuments(text).map(document => {
|
|
7
|
+
if (document.errors.length)
|
|
8
|
+
throw new Error(`${where}: ${document.errors[0].message}`);
|
|
9
|
+
return document.toJS();
|
|
10
|
+
}).filter(document => document != null);
|
|
11
|
+
return fromDocuments(documents, where, where, source);
|
|
12
|
+
}
|
|
13
|
+
/** Manifests from parsed documents, checked for keys used twice. */
|
|
14
|
+
export function fromDocuments(documents, file, where, source = {}) {
|
|
15
|
+
if (!documents.length)
|
|
16
|
+
throw new Error(`${where}: empty`);
|
|
17
|
+
const manifests = documents.map((document, index) => {
|
|
18
|
+
const at = documents.length > 1 ? `${where} (document ${index + 1})` : where;
|
|
19
|
+
return parseManifest(interpolate(document, source.env ?? {}, source.env ? at : `${at}${source.noEnv ? ` (${source.noEnv})` : ""}`), file, at, source);
|
|
20
|
+
});
|
|
21
|
+
const keys = manifests.map(manifest => manifest.key);
|
|
22
|
+
const repeated = keys.find((key, index) => keys.indexOf(key) !== index);
|
|
23
|
+
if (repeated)
|
|
24
|
+
throw new Error(`${where}: the key ${repeated} is used twice`);
|
|
25
|
+
return manifests;
|
|
26
|
+
}
|
|
27
|
+
/** A manifest from its parsed document, with the files it names read through `source.readFile`. */
|
|
28
|
+
export function parseManifest(document, file, where = file, source = {}) {
|
|
29
|
+
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
30
|
+
throw new Error(`${where}: a manifest is a mapping of fields`);
|
|
31
|
+
const { key, agents = [], systemPromptFile, ...definition } = document;
|
|
32
|
+
if (typeof key !== "string" || !KEY.test(key))
|
|
33
|
+
throw new Error(`${where}: key is required, 1 to 80 letters, digits, _ and - (the same key is the same definition)`);
|
|
34
|
+
if (typeof definition.name !== "string" || !definition.name)
|
|
35
|
+
definition.name = key;
|
|
36
|
+
const read = (field, instead, path) => {
|
|
37
|
+
if (!source.readFile)
|
|
38
|
+
throw new Error(`${where}: a hosted deploy reads no files, so ${field} cannot be used; give ${instead}, or deploy with the camelrun CLI`);
|
|
39
|
+
return source.readFile(path);
|
|
40
|
+
};
|
|
41
|
+
if (systemPromptFile !== undefined) {
|
|
42
|
+
if (definition.systemPrompt !== undefined)
|
|
43
|
+
throw new Error(`${where}: give systemPrompt or systemPromptFile, not both`);
|
|
44
|
+
definition.systemPrompt = read("systemPromptFile", "systemPrompt", String(systemPromptFile)).trim();
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(definition.openApi)) {
|
|
47
|
+
definition.openApi = definition.openApi.map((openApi) => {
|
|
48
|
+
if (!openApi || typeof openApi !== "object" || openApi.specFile === undefined)
|
|
49
|
+
return openApi;
|
|
50
|
+
const { specFile, ...rest } = openApi;
|
|
51
|
+
if (rest.spec !== undefined)
|
|
52
|
+
throw new Error(`${where}: an openApi source takes spec or specFile, not both`);
|
|
53
|
+
const text = read("specFile", "spec", String(specFile));
|
|
54
|
+
return { ...rest, spec: /\.json$/i.test(specFile) ? JSON.parse(text) : parseYaml(text) };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(agents))
|
|
58
|
+
throw new Error(`${where}: agents is a list of { key, … }`);
|
|
59
|
+
for (const agent of agents) {
|
|
60
|
+
if (!agent || typeof agent !== "object" || typeof agent.key !== "string" || !KEY.test(agent.key))
|
|
61
|
+
throw new Error(`${where}: each agent needs a key, 1 to 80 letters, digits, _ and -`);
|
|
62
|
+
if ("definition" in agent)
|
|
63
|
+
throw new Error(`${where}: agent ${agent.key} is made from this manifest's definition; leave out definition`);
|
|
64
|
+
}
|
|
65
|
+
return { key, definition, agents, file };
|
|
66
|
+
}
|
|
67
|
+
/** Replace `${NAME}` and `${NAME:-default}` in every string; `$${…}` is left as `${…}`. */
|
|
68
|
+
export function interpolate(value, env, where) {
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
return value.replace(/\$(\$)?\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, escaped, name, fallback) => {
|
|
71
|
+
if (escaped)
|
|
72
|
+
return match.slice(1);
|
|
73
|
+
const found = env[name] ?? fallback;
|
|
74
|
+
if (found === undefined)
|
|
75
|
+
throw new Error(`${where}: \${${name}} is not set`);
|
|
76
|
+
return found;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(value))
|
|
80
|
+
return value.map(item => interpolate(item, env, where));
|
|
81
|
+
if (value && typeof value === "object")
|
|
82
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, interpolate(item, env, where)]));
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
export function template(key, model) {
|
|
86
|
+
return `# An agent on camelRun. Deploy it with \`camelrun deploy\`; deploying again
|
|
87
|
+
# updates the definition in place (the same key is the same definition).
|
|
88
|
+
# Fields: https://agents.camelai.dev/docs/guides/definitions.md
|
|
89
|
+
key: ${key}
|
|
90
|
+
name: ${key}
|
|
91
|
+
${model ? `model: ${model}` : "# model: anthropic/claude-sonnet-5-5 # camelrun models --available lists yours"}
|
|
92
|
+
systemPrompt: |
|
|
93
|
+
You are a helpful assistant. Be concise.
|
|
94
|
+
# systemPromptFile: prompts/${key}.md # or keep the prompt in its own file
|
|
95
|
+
# thinkingLevel: low
|
|
96
|
+
builtins: [web_fetch, web_search]
|
|
97
|
+
# Remote tools. Strings can read environment variables, so credentials stay out of the file.
|
|
98
|
+
# mcpServers:
|
|
99
|
+
# - name: app
|
|
100
|
+
# url: https://app.example.com/mcp
|
|
101
|
+
# auth: { type: bearer, token: "\${APP_MCP_TOKEN}" }
|
|
102
|
+
# openApi:
|
|
103
|
+
# - name: billing
|
|
104
|
+
# specFile: openapi.yaml
|
|
105
|
+
# auth: { type: runtime }
|
|
106
|
+
# Keyed agents to make from this definition on deploy (they live until deleted).
|
|
107
|
+
agents:
|
|
108
|
+
- key: ${key}
|
|
109
|
+
`;
|
|
110
|
+
}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { findManifest, loadManifests } from "./manifest-files.js";
|
|
5
|
+
import { errorText, INSTRUCTIONS, resultText, tools } from "./tools.js";
|
|
6
|
+
import { VERSION } from "./version.js";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
/** Serve the CLI's operations as MCP tools over stdio. Credentials are read on each call, so a login takes effect at once. */
|
|
9
|
+
export async function serve(api, options) {
|
|
10
|
+
const server = createServer(api, options);
|
|
11
|
+
// The transport does not notice its client going away: stdin ending is that.
|
|
12
|
+
const closed = new Promise(resolve => { server.server.onclose = resolve; process.stdin.once("end", resolve); });
|
|
13
|
+
await server.connect(new StdioServerTransport());
|
|
14
|
+
await closed;
|
|
15
|
+
await server.close();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The tools (tools.ts) as an MCP server, for `api`. With `cwd` it runs on the user's machine: deploy reads manifests
|
|
19
|
+
* and the files they name, and `${NAME}` reads the environment. Without it (the runtime's hosted /mcp), deploy takes
|
|
20
|
+
* the manifest's YAML only, and reads no file and no environment variable of the server's.
|
|
21
|
+
*/
|
|
22
|
+
export function createServer(api, options = {}) {
|
|
23
|
+
const { cwd } = options;
|
|
24
|
+
const server = new McpServer({ name: "camelrun", title: "camelRun", version: VERSION }, { instructions: INSTRUCTIONS });
|
|
25
|
+
const local = cwd === undefined ? undefined : {
|
|
26
|
+
env: process.env,
|
|
27
|
+
readFile: (path) => readFileSync(resolve(cwd, path), "utf8"),
|
|
28
|
+
load: (file) => loadManifests(file ? (isAbsolute(file) ? file : resolve(cwd, file)) : findManifest(cwd)),
|
|
29
|
+
};
|
|
30
|
+
for (const tool of tools(api, { local })) {
|
|
31
|
+
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.input, annotations: tool.annotations }, (async (args) => {
|
|
32
|
+
try {
|
|
33
|
+
return { content: [{ type: "text", text: resultText(await tool.run(args)) }] };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return { isError: true, content: [{ type: "text", text: errorText(error) }] };
|
|
37
|
+
}
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
return server;
|
|
41
|
+
}
|
package/dist/ops.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { ApiError, enc } from "./api.js";
|
|
2
|
+
/** Deploy manifests: upsert each definition by its key, apply it to live agents if asked, and make its keyed agents. */
|
|
3
|
+
export async function deploy(api, manifests, options = {}) {
|
|
4
|
+
const results = [];
|
|
5
|
+
for (const manifest of manifests) {
|
|
6
|
+
const id = await api.definitionId(manifest.key);
|
|
7
|
+
const before = await api.get(`/v1/definitions/${id}`).catch(error => { if (error instanceof ApiError && error.status === 404)
|
|
8
|
+
return undefined; throw error; });
|
|
9
|
+
if (options.dryRun) {
|
|
10
|
+
results.push({ key: manifest.key, id, name: String(manifest.definition.name), revision: before?.revision ?? 0, status: "planned", agents: manifest.agents.map(agent => ({ key: agent.key, status: "planned" })) });
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const saved = await api.call("POST", "/v1/definitions", manifest.definition, { "Idempotency-Key": manifest.key });
|
|
14
|
+
const result = {
|
|
15
|
+
key: manifest.key, id: saved.id, name: saved.name, revision: saved.revision,
|
|
16
|
+
status: !before ? "created" : before.revision === saved.revision ? "unchanged" : "updated",
|
|
17
|
+
...(saved.toolSources ? { toolSources: saved.toolSources } : {}),
|
|
18
|
+
agents: [],
|
|
19
|
+
};
|
|
20
|
+
if (options.apply && result.status !== "created")
|
|
21
|
+
result.applied = (await api.call("PATCH", `/v1/definitions/${saved.id}`, { apply: "all" })).applied ?? [];
|
|
22
|
+
for (const { key, ...config } of manifest.agents) {
|
|
23
|
+
try {
|
|
24
|
+
const agent = await api.call("POST", "/v1/agents", { definition: saved.id, ...config }, { "Idempotency-Key": key });
|
|
25
|
+
result.agents.push({ key, id: agent.id, status: "ready", ...(agent.reconfigured ? { reconfigured: true } : {}) });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
result.agents.push({ key, status: "failed", error: error.message });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
results.push(result);
|
|
32
|
+
}
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
/** The definition a manifest would send, with credentials hidden, for a dry run. */
|
|
36
|
+
export function redact(value) {
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return value.map(redact);
|
|
39
|
+
if (value && typeof value === "object") {
|
|
40
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, /^(token|apiKey|secret|password)$/i.test(key) || (key === "headers" && item && typeof item === "object") ? redactAll(item) : redact(item)]));
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
const redactAll = (value) => value && typeof value === "object" ? Object.fromEntries(Object.keys(value).map(key => [key, "***"])) : "***";
|
|
45
|
+
/** A request record as a run: what it said, what it waits on, or why it failed. */
|
|
46
|
+
export function summarize(agent, record) {
|
|
47
|
+
const base = { agent, requestId: record.id };
|
|
48
|
+
if (record.state !== "completed")
|
|
49
|
+
return { ...base, status: "running" };
|
|
50
|
+
const outcome = record.outcome ?? {};
|
|
51
|
+
const result = outcome.result ?? {};
|
|
52
|
+
const failure = outcome.error ? { code: outcome.uncertain ? "uncertain" : "runtime_error", message: outcome.error }
|
|
53
|
+
: result.error ? { code: result.code ?? "model_error", message: result.error }
|
|
54
|
+
: result.stopped === "spend_limit" ? { code: "spend_limit", message: "The agent reached its spend limit; raise it to go on" } : undefined;
|
|
55
|
+
const status = failure ? "failed" : result.stopped === "input_required" ? "input_required" : "completed";
|
|
56
|
+
return {
|
|
57
|
+
...base, status,
|
|
58
|
+
...(typeof result.reply === "string" ? { text: result.reply } : {}),
|
|
59
|
+
...(failure ? { error: failure } : {}),
|
|
60
|
+
...(result.inputs?.length ? { inputs: result.inputs.map((input) => ({ id: input.id, kind: input.kind, ...(input.message ? { message: input.message } : {}), detail: input.detail })) } : {}),
|
|
61
|
+
...(result.toolErrors?.length ? { toolErrors: result.toolErrors } : {}),
|
|
62
|
+
...(result.files?.length ? { files: result.files } : {}),
|
|
63
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Wait up to `seconds` for a request to end; a run still going then comes back `running`. */
|
|
67
|
+
export async function waitFor(api, agentId, requestId, seconds, signal) {
|
|
68
|
+
const deadline = Date.now() + seconds * 1000;
|
|
69
|
+
for (let delay = 250;; delay = Math.min(delay * 1.5, 2000)) {
|
|
70
|
+
const record = await api.get(`/v1/agents/${enc(agentId)}/requests/${enc(requestId)}`);
|
|
71
|
+
if (record.state === "completed" || Date.now() + delay > deadline || signal?.aborted)
|
|
72
|
+
return summarize(agentId, record);
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Send an agent a message and wait up to `wait` seconds for its run. `requestId` makes a retry the same run. */
|
|
77
|
+
export async function run(api, agent, text, options = {}) {
|
|
78
|
+
const id = await api.agentId(agent);
|
|
79
|
+
const record = await api.call("POST", `/v1/agents/${enc(id)}/prompt`, {
|
|
80
|
+
text, requestId: options.requestId ?? `cli_${globalThis.crypto.randomUUID()}`, ...(options.allowDisconnected ? { allowDisconnected: true } : {}),
|
|
81
|
+
...(options.from ? { from: { id: options.from } } : {}), ...(options.steer ? { whileRunning: "steer" } : {}),
|
|
82
|
+
});
|
|
83
|
+
return options.wait === 0 ? summarize(id, record) : waitFor(api, id, record.id, options.wait ?? Infinity, options.signal);
|
|
84
|
+
}
|
|
85
|
+
/** Answer an input the way the SDK does: true/false for an approval, text or { question: answer } for questions, fields for a form. */
|
|
86
|
+
export async function answer(api, agent, inputId, value, options = {}) {
|
|
87
|
+
const id = await api.agentId(agent);
|
|
88
|
+
const inputs = await api.get(`/v1/agents/${enc(id)}/inputs`);
|
|
89
|
+
const input = inputs.find(item => item.id === inputId);
|
|
90
|
+
if (!input)
|
|
91
|
+
throw new ApiError(404, `No input ${inputId} on ${agent}`);
|
|
92
|
+
const body = { ...answerFor(input, value), ...(options.from ? { from: { id: options.from } } : {}) };
|
|
93
|
+
const answered = await api.call("POST", `/v1/agents/${enc(id)}/inputs/${enc(inputId)}`, body);
|
|
94
|
+
if (!answered.request)
|
|
95
|
+
return { agent: id, input: inputId, status: "answered", note: "Other inputs of this run still wait; it resumes once they are answered" };
|
|
96
|
+
return options.wait === 0 ? summarize(id, answered.request) : waitFor(api, id, answered.request.id, options.wait ?? Infinity);
|
|
97
|
+
}
|
|
98
|
+
function answerFor(input, value) {
|
|
99
|
+
if (value === "decline" || (value === false && input.kind !== "approval" && input.kind !== "url"))
|
|
100
|
+
return { action: "decline" };
|
|
101
|
+
switch (input.kind) {
|
|
102
|
+
case "approval":
|
|
103
|
+
case "url":
|
|
104
|
+
if (typeof value !== "boolean")
|
|
105
|
+
throw new Error(`Answer ${input.kind === "approval" ? "an approval" : "a url step"} with true or false`);
|
|
106
|
+
return { action: value ? "accept" : "decline" };
|
|
107
|
+
case "question": {
|
|
108
|
+
const questions = input.detail?.questions ?? [];
|
|
109
|
+
if (typeof value === "string" || Array.isArray(value)) {
|
|
110
|
+
if (questions.length !== 1)
|
|
111
|
+
throw new Error(`This input asks ${questions.length} questions: answer with {"<question>": "<answer>"} for each`);
|
|
112
|
+
return { action: "accept", content: { answers: { [questions[0].question]: value } } };
|
|
113
|
+
}
|
|
114
|
+
return { action: "accept", content: { answers: value } };
|
|
115
|
+
}
|
|
116
|
+
default: return { action: "accept", content: value };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** An agent's recent messages, flattened to role and text (tool calls by name), oldest first. */
|
|
120
|
+
export async function history(api, agent, limit = 20) {
|
|
121
|
+
const id = await api.agentId(agent);
|
|
122
|
+
const page = await api.get(`/v1/agents/${enc(id)}/history?limit=${Math.max(1, Math.min(500, limit))}`);
|
|
123
|
+
return { agent: id, total: page.total, messages: page.entries.map((entry) => ({ index: entry.index, ...flatten(entry.message) })) };
|
|
124
|
+
}
|
|
125
|
+
function flatten(message) {
|
|
126
|
+
const content = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content ?? [];
|
|
127
|
+
const text = content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
128
|
+
const calls = content.filter((part) => part.type === "toolCall").map((part) => ({ name: part.name, arguments: part.arguments }));
|
|
129
|
+
return {
|
|
130
|
+
role: message.role,
|
|
131
|
+
...(message.toolName ? { tool: message.toolName } : {}),
|
|
132
|
+
...(message.from ? { from: message.from.id } : {}),
|
|
133
|
+
...(text ? { text: message.role === "toolResult" && text.length > 2000 ? `${text.slice(0, 2000)}…` : text } : {}),
|
|
134
|
+
...(calls.length ? { toolCalls: calls } : {}),
|
|
135
|
+
...(message.isError ? { isError: true } : {}),
|
|
136
|
+
...(message.errorMessage ? { error: message.errorMessage } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** An agent for `key` made (or found) outside a manifest: from a definition, or with its own model and prompt. */
|
|
140
|
+
export async function upsertAgent(api, key, config) {
|
|
141
|
+
const { definition, ...rest } = config;
|
|
142
|
+
const body = { ...(definition ? { definition: await api.definitionId(definition) } : {}), ...Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined)) };
|
|
143
|
+
const created = await api.call("POST", "/v1/agents", body, { "Idempotency-Key": key });
|
|
144
|
+
return { key, id: created.id, expiresAt: created.expiresAt, ...(created.reconfigured ? { reconfigured: true } : {}) };
|
|
145
|
+
}
|
|
146
|
+
/** Change one agent's model, prompt or thinking level between its runs, and wait up to `wait` seconds for it to land. */
|
|
147
|
+
export async function configure(api, agent, changes, wait = 30) {
|
|
148
|
+
const id = await api.agentId(agent);
|
|
149
|
+
const record = await api.call("PATCH", `/v1/agents/${enc(id)}/configuration`, { requestId: `cli_${globalThis.crypto.randomUUID()}`, ...changes });
|
|
150
|
+
return waitFor(api, id, record.id, wait);
|
|
151
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { enc } from "./api.js";
|
|
3
|
+
import { parseManifests } from "./manifest.js";
|
|
4
|
+
import * as ops from "./ops.js";
|
|
5
|
+
export const INSTRUCTIONS = `Deploy and manage agents on camelRun (https://agents.camelai.dev).
|
|
6
|
+
|
|
7
|
+
- An agent is durable and keyed: the same key is the same agent, with its history, until deleted. Tools take an agent's key or its id (client_…).
|
|
8
|
+
- A definition is a reusable configuration (model, system prompt, built-ins, MCP servers, OpenAPI specs). Keep it in the repository as agent.yaml and deploy it with deploy: the same key is the same definition, and deploying again makes a new revision. apply: true also moves live agents to it between their turns.
|
|
9
|
+
- run_agent sends a message and waits up to wait seconds. A run still going comes back with status "running": call get_run with its requestId. A run with status "input_required" waits on a person: show them its inputs, then answer_input.
|
|
10
|
+
- read_docs reads the runtime's documentation (the manifest's fields are in guides/definitions.md).`;
|
|
11
|
+
const agent = z.string().describe("The agent's key, or its id (client_…)");
|
|
12
|
+
const definition = z.string().describe("The definition's key, or its id (def_…)");
|
|
13
|
+
const wait = z.number().min(0).max(300).optional().describe("Seconds to wait for the run to end (default 50); a run still going comes back as running");
|
|
14
|
+
/** Every tool, for `api`. */
|
|
15
|
+
export function tools(api, options = {}) {
|
|
16
|
+
const list = [];
|
|
17
|
+
const tool = (name, description, input, run, annotations = {}) => {
|
|
18
|
+
list.push({ name, description, input, annotations, run });
|
|
19
|
+
};
|
|
20
|
+
const read = { readOnlyHint: true };
|
|
21
|
+
tool("whoami", "The tenant the API key belongs to, the runtime's URL and the default model.", {}, async () => {
|
|
22
|
+
const client = api();
|
|
23
|
+
return { ...await client.me(), url: client.url };
|
|
24
|
+
}, read);
|
|
25
|
+
tool("list_models", "Models agents can use: pass a model's id as `model`.", { available: z.boolean().optional().describe("Only models the account has a key for (default true)") }, async ({ available }) => {
|
|
26
|
+
const models = await api().get(`/v1/models${available === false ? "" : "?available=true"}`);
|
|
27
|
+
return models.map(({ id, name, contextWindow, reasoning, cost, available }) => ({ id, name, available, contextWindow, reasoning, cost }));
|
|
28
|
+
}, read);
|
|
29
|
+
const local = options.local;
|
|
30
|
+
const hosted = !local;
|
|
31
|
+
tool("deploy", "Deploy agent manifests: upsert each definition by its key (a new revision only when it changed) and make the keyed agents it lists. " +
|
|
32
|
+
(hosted
|
|
33
|
+
? "Give `manifest`, the YAML itself (as an agent.yaml holds it). This server reads no files and no environment variables: put the system prompt in systemPrompt and any OpenAPI spec in spec."
|
|
34
|
+
: "Give `file` (a path to an agent.yaml; default ./agent.yaml) or `manifest` (the YAML itself). `${NAME}` in strings reads this server's environment."), {
|
|
35
|
+
...(hosted ? {} : { file: z.string().optional().describe("Path to the manifest, relative to the working directory") }),
|
|
36
|
+
manifest: hosted ? z.string().describe("The manifest's YAML") : z.string().optional().describe("The manifest's YAML, instead of a file"),
|
|
37
|
+
apply: z.boolean().optional().describe("Also move live agents made from the definition to the new revision, between their turns"),
|
|
38
|
+
dryRun: z.boolean().optional().describe("Show what would be deployed, credentials hidden, without changing anything"),
|
|
39
|
+
}, async (args) => {
|
|
40
|
+
const { file, manifest, apply, dryRun } = args;
|
|
41
|
+
if (file && manifest)
|
|
42
|
+
throw new Error("Give file or manifest, not both");
|
|
43
|
+
const manifests = manifest !== undefined
|
|
44
|
+
? parseManifests(manifest, "manifest", local
|
|
45
|
+
? { env: local.env, readFile: local.readFile }
|
|
46
|
+
: { noEnv: "a hosted deploy reads no environment variables: write the value in, or deploy with the camelrun CLI" })
|
|
47
|
+
: local.load(file);
|
|
48
|
+
const results = await ops.deploy(api(), manifests, { apply, dryRun });
|
|
49
|
+
return dryRun ? results.map((result, index) => ({ ...result, definition: ops.redact(manifests[index].definition) })) : results;
|
|
50
|
+
});
|
|
51
|
+
tool("list_definitions", "The account's definitions.", {}, async () => {
|
|
52
|
+
const list = await api().get("/v1/definitions");
|
|
53
|
+
return list.map(({ id, name, revision, model, builtins, updatedAt }) => ({ id, name, revision, model, builtins, updatedAt }));
|
|
54
|
+
}, read);
|
|
55
|
+
tool("get_definition", "A definition in full: model, prompt, built-ins and tool sources (never their credentials).", { definition }, async ({ definition }) => {
|
|
56
|
+
const client = api();
|
|
57
|
+
return client.get(`/v1/definitions/${await client.definitionId(definition)}`);
|
|
58
|
+
}, read);
|
|
59
|
+
tool("definition_agents", "The agents made from a definition, and the revision each has.", { definition }, async ({ definition }) => {
|
|
60
|
+
const client = api();
|
|
61
|
+
return client.get(`/v1/definitions/${await client.definitionId(definition)}/agents`);
|
|
62
|
+
}, read);
|
|
63
|
+
tool("delete_definition", "Delete a definition. Its agents are left as they are.", { definition }, async ({ definition }) => {
|
|
64
|
+
const client = api();
|
|
65
|
+
return client.call("DELETE", `/v1/definitions/${await client.definitionId(definition)}`);
|
|
66
|
+
}, { destructiveHint: true });
|
|
67
|
+
tool("list_agents", "The account's agents: key, id, model, and whether each is running.", {}, () => api().get("/v1/agents"), read);
|
|
68
|
+
tool("get_agent", "An agent's configuration, the definition revision it has, and every source of its tools with what each offers.", { agent }, async ({ agent }) => {
|
|
69
|
+
const client = api();
|
|
70
|
+
return client.get(`/v1/agents/${enc(await client.agentId(agent))}`);
|
|
71
|
+
}, read);
|
|
72
|
+
tool("create_agent", "Make the agent for a key, or find it: from a definition, or with its own model and system prompt. The same key is the same agent; it lives until deleted.", {
|
|
73
|
+
key: z.string().regex(/^[A-Za-z0-9_-]{1,80}$/).describe("Your name for the agent: 1 to 80 letters, digits, _ and -"),
|
|
74
|
+
definition: definition.optional(),
|
|
75
|
+
model: z.string().optional().describe("A model id from list_models"),
|
|
76
|
+
systemPrompt: z.string().optional().describe("Not with a definition, which owns the prompt"),
|
|
77
|
+
systemPromptAppend: z.string().optional().describe("Text after the prompt, e.g. this agent's own context"),
|
|
78
|
+
name: z.string().optional(),
|
|
79
|
+
builtins: z.array(z.enum(["web_fetch", "web_search", "schedule", "ask_user"])).optional().describe("Without a definition: tools the runtime answers itself"),
|
|
80
|
+
}, ({ key, ...config }) => ops.upsertAgent(api(), key, config), { idempotentHint: true });
|
|
81
|
+
tool("configure_agent", "Change one agent's model, system prompt, prompt addition or thinking level between its runs; its history is kept.", {
|
|
82
|
+
agent,
|
|
83
|
+
model: z.string().optional(),
|
|
84
|
+
systemPrompt: z.string().optional(),
|
|
85
|
+
systemPromptAppend: z.string().optional().describe("\"\" removes it"),
|
|
86
|
+
thinkingLevel: z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]).optional(),
|
|
87
|
+
}, ({ agent, ...changes }) => {
|
|
88
|
+
const given = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== undefined));
|
|
89
|
+
if (!Object.keys(given).length)
|
|
90
|
+
throw new Error("Nothing to change");
|
|
91
|
+
return ops.configure(api(), agent, given);
|
|
92
|
+
});
|
|
93
|
+
tool("delete_agent", "Delete an agent: it stops at once, and its history and files are purged. Its key makes a fresh agent next time.", { agent }, async ({ agent }) => {
|
|
94
|
+
const client = api();
|
|
95
|
+
return client.call("DELETE", `/v1/agents/${enc(await client.agentId(agent))}`);
|
|
96
|
+
}, { destructiveHint: true });
|
|
97
|
+
tool("run_agent", "Send an agent a message and wait for its run: the reply, the input it waits on, or why it failed. A run still going after `wait` seconds comes back as running; call get_run.", {
|
|
98
|
+
agent, message: z.string(), wait,
|
|
99
|
+
from: z.string().optional().describe("Who is sending, as a user id in the application"),
|
|
100
|
+
steer: z.boolean().optional().describe("If a run is going, have it read this after its current step instead of queuing it"),
|
|
101
|
+
allowDisconnected: z.boolean().optional().describe("Run even though the application serving its attached tools is not connected; those calls then fail"),
|
|
102
|
+
requestId: z.string().optional().describe("Retrying with the same id is the same run"),
|
|
103
|
+
}, ({ agent, message, wait, ...options }) => ops.run(api(), agent, message, { ...options, wait: wait ?? 50 }));
|
|
104
|
+
tool("get_run", "A run's status and result, waiting up to `wait` seconds for it to end.", { agent, requestId: z.string(), wait }, async ({ agent, requestId, wait }) => {
|
|
105
|
+
const client = api();
|
|
106
|
+
return ops.waitFor(client, await client.agentId(agent), requestId, wait ?? 50);
|
|
107
|
+
}, read);
|
|
108
|
+
tool("agent_history", "An agent's latest messages, in whole turns: text, tool calls and tool results.", { agent, limit: z.number().int().min(1).max(500).optional().describe("Default 20") }, ({ agent, limit }) => ops.history(api(), agent, limit), read);
|
|
109
|
+
tool("abort_agent", "Stop an agent's running turn.", { agent }, async ({ agent }) => {
|
|
110
|
+
const client = api();
|
|
111
|
+
return client.call("POST", `/v1/agents/${enc(await client.agentId(agent))}/abort`, {});
|
|
112
|
+
});
|
|
113
|
+
tool("list_inputs", "Questions, approvals and forms waiting on a person: one agent's, or every agent's.", { agent: agent.optional() }, async ({ agent }) => {
|
|
114
|
+
const client = api();
|
|
115
|
+
return agent ? client.get(`/v1/agents/${enc(await client.agentId(agent))}/inputs?state=pending`) : client.get("/v1/inputs?state=pending");
|
|
116
|
+
}, read);
|
|
117
|
+
tool("answer_input", "Answer an input and wait for the run it resumes. An approval takes true or false; a question the chosen label or your own words (several questions: {\"<question>\": \"<answer>\"}); a form its fields; \"decline\" declines. Only answer with what the person decided.", { agent, inputId: z.string(), value: z.union([z.boolean(), z.string(), z.array(z.string()), z.record(z.string(), z.unknown())]), from: z.string().optional().describe("Who is answering, as a user id in the application"), wait }, ({ agent, inputId, value, from, wait }) => ops.answer(api(), agent, inputId, value, { from, wait: wait ?? 50 }));
|
|
118
|
+
tool("list_schedules", "An agent's scheduled wake-ups.", { agent }, async ({ agent }) => {
|
|
119
|
+
const client = api();
|
|
120
|
+
return client.get(`/v1/agents/${enc(await client.agentId(agent))}/schedules`);
|
|
121
|
+
}, read);
|
|
122
|
+
tool("add_schedule", "Wake an agent later with a message: once (inSeconds or at), or repeatedly (everySeconds, at least 60).", {
|
|
123
|
+
agent, text: z.string(),
|
|
124
|
+
inSeconds: z.number().optional(), at: z.string().optional().describe("ISO time"), everySeconds: z.number().int().min(60).optional(),
|
|
125
|
+
}, async ({ agent, ...schedule }) => {
|
|
126
|
+
const client = api();
|
|
127
|
+
return client.call("POST", `/v1/agents/${enc(await client.agentId(agent))}/schedules`, schedule);
|
|
128
|
+
});
|
|
129
|
+
tool("delete_schedule", "Cancel a scheduled wake-up.", { agent, scheduleId: z.string() }, async ({ agent, scheduleId }) => {
|
|
130
|
+
const client = api();
|
|
131
|
+
return client.call("DELETE", `/v1/agents/${enc(await client.agentId(agent))}/schedules/${enc(scheduleId)}`);
|
|
132
|
+
}, { destructiveHint: true });
|
|
133
|
+
tool("read_docs", "The runtime's documentation as Markdown: with no path, the index (llms.txt); else a page such as guides/definitions.md, guides/tools.md or reference/cli.md.", {
|
|
134
|
+
path: z.string().regex(/^(?!.*\.\.)[A-Za-z0-9/_.-]*$/).optional(),
|
|
135
|
+
}, ({ path }) => api().text(path ? `/docs/${path.replace(/^\/?(docs\/)?/, "")}` : "/llms.txt"), read);
|
|
136
|
+
return list;
|
|
137
|
+
}
|
|
138
|
+
/** A tool's result as text, as MCP and WebMCP clients take it. */
|
|
139
|
+
export const resultText = (result) => typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
140
|
+
/** An error as the model should read it: the HTTP status, when there was one, and the message. */
|
|
141
|
+
export const errorText = (error) => `${error?.status ? `${error.status}: ` : ""}${error.message}`;
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1 +1,37 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@camelai/camelrun",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "camelRun CLI: deploy and manage agents on camelRun from a terminal, or from a coding agent over MCP (camelrun mcp).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/qaml-ai/run.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://agents.camelai.dev",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"registry": "https://registry.npmjs.org",
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"camelrun": "dist/bin.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/bin.js",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
34
|
+
"yaml": "^2.9.0",
|
|
35
|
+
"zod": "^4.6.5"
|
|
36
|
+
}
|
|
37
|
+
}
|