akemon 0.1.6 → 0.1.8

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.
@@ -1,6 +1,33 @@
1
1
  import WebSocket from "ws";
2
2
  import http from "http";
3
3
  const DEFAULT_RELAY_URL = "wss://relay.akemon.dev";
4
+ // Pending agent_call results (callId → resolve function)
5
+ const pendingAgentCalls = new Map();
6
+ let relayWsRef = null;
7
+ /** Call another agent through the relay. Available to any engine. */
8
+ export function callAgent(target, task) {
9
+ return new Promise((resolve, reject) => {
10
+ if (!relayWsRef || relayWsRef.readyState !== WebSocket.OPEN) {
11
+ reject(new Error("Not connected to relay"));
12
+ return;
13
+ }
14
+ const callId = Math.random().toString(36).slice(2) + Date.now().toString(36);
15
+ pendingAgentCalls.set(callId, resolve);
16
+ relayWsRef.send(JSON.stringify({
17
+ type: "agent_call",
18
+ call_id: callId,
19
+ target,
20
+ task,
21
+ }));
22
+ // Timeout after 5 minutes
23
+ setTimeout(() => {
24
+ if (pendingAgentCalls.has(callId)) {
25
+ pendingAgentCalls.delete(callId);
26
+ reject(new Error(`agent_call to ${target} timed out`));
27
+ }
28
+ }, 300_000);
29
+ });
30
+ }
4
31
  export function connectRelay(options) {
5
32
  const relayUrl = options.relayUrl || DEFAULT_RELAY_URL;
6
33
  let wsUrl = relayUrl.replace(/^http/, "ws");
@@ -52,6 +79,7 @@ export function connectRelay(options) {
52
79
  ws.on("open", () => {
53
80
  console.log(`[relay-ws] Connected. Registering agent "${options.agentName}"...`);
54
81
  reconnectDelay = 1000; // reset backoff
82
+ relayWsRef = ws;
55
83
  // Send registration message
56
84
  const reg = {
57
85
  type: "register",
@@ -90,6 +118,15 @@ export function connectRelay(options) {
90
118
  case "mcp_request":
91
119
  handleMCPRequest(ws, msg, options.localPort);
92
120
  break;
121
+ case "control":
122
+ handleControl(ws, msg);
123
+ break;
124
+ case "agent_call":
125
+ handleIncomingAgentCall(ws, msg, options.localPort);
126
+ break;
127
+ case "agent_call_result":
128
+ handleAgentCallResult(msg);
129
+ break;
93
130
  default:
94
131
  console.log(`[relay-ws] Unknown message type: ${msg.type}`);
95
132
  }
@@ -113,6 +150,120 @@ export function connectRelay(options) {
113
150
  }
114
151
  connect();
115
152
  }
153
+ function handleIncomingAgentCall(ws, msg, localPort) {
154
+ const callId = msg.call_id || "";
155
+ const caller = msg.caller || "unknown";
156
+ const task = msg.task || "";
157
+ console.log(`[agent_call] Incoming from ${caller}: ${task.slice(0, 80)}`);
158
+ // Forward to local MCP as a submit_task call
159
+ const initBody = JSON.stringify({
160
+ jsonrpc: "2.0", id: 1,
161
+ method: "initialize",
162
+ params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "agent-call", version: "1.0" } },
163
+ });
164
+ const doRequest = (body, sessionId) => {
165
+ return new Promise((resolve, reject) => {
166
+ const headers = {
167
+ "Content-Type": "application/json",
168
+ "Accept": "application/json, text/event-stream",
169
+ };
170
+ if (sessionId)
171
+ headers["mcp-session-id"] = sessionId;
172
+ const req = http.request({ hostname: "127.0.0.1", port: localPort, path: "/mcp", method: "POST", headers: { ...headers, "Content-Length": Buffer.byteLength(body) } }, (res) => {
173
+ const chunks = [];
174
+ res.on("data", (c) => chunks.push(c));
175
+ res.on("end", () => {
176
+ const sid = res.headers["mcp-session-id"];
177
+ resolve({ data: Buffer.concat(chunks).toString(), sessionId: sid || sessionId });
178
+ });
179
+ });
180
+ req.on("error", reject);
181
+ req.write(body);
182
+ req.end();
183
+ });
184
+ };
185
+ // Initialize → call tool → return result
186
+ doRequest(initBody)
187
+ .then(({ sessionId: sid }) => {
188
+ const callBody = JSON.stringify({
189
+ jsonrpc: "2.0", id: 2,
190
+ method: "tools/call",
191
+ params: { name: "submit_task", arguments: { task } },
192
+ });
193
+ return doRequest(callBody, sid);
194
+ })
195
+ .then(({ data }) => {
196
+ // Extract text from SSE or JSON response
197
+ let result = data;
198
+ try {
199
+ // Try SSE extraction
200
+ const lines = data.split("\n");
201
+ let lastData = "";
202
+ for (const line of lines) {
203
+ if (line.startsWith("data: "))
204
+ lastData = line.slice(6);
205
+ }
206
+ if (lastData) {
207
+ const parsed = JSON.parse(lastData);
208
+ const content = parsed?.result?.content;
209
+ if (content)
210
+ result = content.map((c) => c.text || "").join("\n");
211
+ }
212
+ else {
213
+ const parsed = JSON.parse(data);
214
+ const content = parsed?.result?.content;
215
+ if (content)
216
+ result = content.map((c) => c.text || "").join("\n");
217
+ }
218
+ }
219
+ catch { /* use raw */ }
220
+ ws.send(JSON.stringify({
221
+ type: "agent_call_result",
222
+ call_id: callId,
223
+ caller,
224
+ result,
225
+ }));
226
+ console.log(`[agent_call] Replied to ${caller} (${result.length} bytes)`);
227
+ })
228
+ .catch((err) => {
229
+ ws.send(JSON.stringify({
230
+ type: "agent_call_result",
231
+ call_id: callId,
232
+ caller,
233
+ result: `[error] ${err.message}`,
234
+ }));
235
+ });
236
+ }
237
+ function handleAgentCallResult(msg) {
238
+ const callId = msg.call_id || "";
239
+ const resolve = pendingAgentCalls.get(callId);
240
+ if (resolve) {
241
+ pendingAgentCalls.delete(callId);
242
+ resolve(msg.result || "");
243
+ console.log(`[agent_call] Got result for call_id=${callId.slice(0, 8)} from ${msg.caller}`);
244
+ }
245
+ }
246
+ function handleControl(ws, msg) {
247
+ const action = msg.action || "";
248
+ console.log(`[control] Received: ${action}`);
249
+ switch (action) {
250
+ case "shutdown":
251
+ console.log("[control] Shutting down by remote command...");
252
+ ws.send(JSON.stringify({ type: "control_ack", action }));
253
+ setTimeout(() => process.exit(0), 500);
254
+ break;
255
+ case "set_public":
256
+ console.log("[control] Agent set to public by remote command");
257
+ ws.send(JSON.stringify({ type: "control_ack", action }));
258
+ break;
259
+ case "set_private":
260
+ console.log("[control] Agent set to private by remote command");
261
+ ws.send(JSON.stringify({ type: "control_ack", action }));
262
+ break;
263
+ default:
264
+ console.log(`[control] Unknown action: ${action}`);
265
+ }
266
+ }
116
267
  function handleMCPRequest(ws, msg, localPort) {
117
268
  const requestId = msg.request_id;
118
269
  console.log(`[relay-ws] → mcp_request ${requestId}`);
package/dist/server.js CHANGED
@@ -2,9 +2,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { z } from "zod";
5
- import { spawn } from "child_process";
5
+ import { spawn, exec } from "child_process";
6
6
  import { createServer } from "http";
7
7
  import { createInterface } from "readline";
8
+ import { callAgent } from "./relay-client.js";
8
9
  function runCommand(cmd, args, task, cwd, stdinMode = true) {
9
10
  return new Promise((resolve, reject) => {
10
11
  const { CLAUDECODE, ...cleanEnv } = process.env;
@@ -45,6 +46,19 @@ function runCommand(cmd, args, task, cwd, stdinMode = true) {
45
46
  child.on("error", reject);
46
47
  });
47
48
  }
49
+ function runTerminal(command, cwd) {
50
+ return new Promise((resolve) => {
51
+ exec(command, { cwd, timeout: 300_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
52
+ const output = (stdout || "") + (stderr ? "\n[stderr]\n" + stderr : "");
53
+ if (err && !output.trim()) {
54
+ resolve(`[error] ${err.message}`);
55
+ }
56
+ else {
57
+ resolve(output.trim() || "[no output]");
58
+ }
59
+ });
60
+ });
61
+ }
48
62
  // stdinMode: true = send task via stdin, false = send task as argument
49
63
  function buildEngineCommand(engine, model, allowAll) {
50
64
  switch (engine) {
@@ -198,8 +212,15 @@ function createMcpServer(opts) {
198
212
  console.log(`[approve] Owner approved. Executing with ${engine}...`);
199
213
  }
200
214
  try {
201
- const { cmd, args, stdinMode } = buildEngineCommand(engine, model, allowAll);
202
- const output = await runCommand(cmd, args, safeTask, workdir, stdinMode);
215
+ let output;
216
+ if (engine === "terminal") {
217
+ console.log(`[terminal] Executing: ${task}`);
218
+ output = await runTerminal(task, workdir);
219
+ }
220
+ else {
221
+ const { cmd, args, stdinMode } = buildEngineCommand(engine, model, allowAll);
222
+ output = await runCommand(cmd, args, safeTask, workdir, stdinMode);
223
+ }
203
224
  // Store updated context
204
225
  if (contextEnabled && publisherId) {
205
226
  const newContext = buildContextPayload(prevContext, task, output);
@@ -217,6 +238,25 @@ function createMcpServer(opts) {
217
238
  };
218
239
  }
219
240
  });
241
+ // Agent-to-agent calling tool
242
+ server.tool("call_agent", "Call another akemon agent by name. The target agent will execute the task and return the result. Use this to delegate subtasks to specialized agents.", {
243
+ agent: z.string().describe("Name of the target agent to call"),
244
+ task: z.string().describe("Task to send to the target agent"),
245
+ }, async ({ agent: target, task }) => {
246
+ console.log(`[call_agent] ${agentName} → ${target}: ${task.slice(0, 80)}`);
247
+ try {
248
+ const result = await callAgent(target, task);
249
+ return {
250
+ content: [{ type: "text", text: result }],
251
+ };
252
+ }
253
+ catch (err) {
254
+ return {
255
+ content: [{ type: "text", text: `[error] Failed to call agent "${target}": ${err.message}` }],
256
+ isError: true,
257
+ };
258
+ }
259
+ });
220
260
  return server;
221
261
  }
222
262
  export async function serve(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",