@agentmeshhq/agent 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 +111 -0
- package/dist/cli/attach.d.ts +1 -0
- package/dist/cli/attach.js +18 -0
- package/dist/cli/attach.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +98 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +55 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/list.d.ts +1 -0
- package/dist/cli/list.js +45 -0
- package/dist/cli/list.js.map +1 -0
- package/dist/cli/nudge.d.ts +1 -0
- package/dist/cli/nudge.js +72 -0
- package/dist/cli/nudge.js.map +1 -0
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +37 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/stop.d.ts +1 -0
- package/dist/cli/stop.js +33 -0
- package/dist/cli/stop.js.map +1 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.js +65 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +32 -0
- package/dist/config/schema.js +11 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/daemon.d.ts +20 -0
- package/dist/core/daemon.js +164 -0
- package/dist/core/daemon.js.map +1 -0
- package/dist/core/heartbeat.d.ts +14 -0
- package/dist/core/heartbeat.js +42 -0
- package/dist/core/heartbeat.js.map +1 -0
- package/dist/core/injector.d.ts +8 -0
- package/dist/core/injector.js +84 -0
- package/dist/core/injector.js.map +1 -0
- package/dist/core/registry.d.ts +27 -0
- package/dist/core/registry.js +52 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/tmux.d.ts +11 -0
- package/dist/core/tmux.js +112 -0
- package/dist/core/tmux.js.map +1 -0
- package/dist/core/websocket.d.ts +25 -0
- package/dist/core/websocket.js +65 -0
- package/dist/core/websocket.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
- package/src/cli/attach.ts +22 -0
- package/src/cli/index.ts +101 -0
- package/src/cli/init.ts +87 -0
- package/src/cli/list.ts +62 -0
- package/src/cli/nudge.ts +84 -0
- package/src/cli/start.ts +50 -0
- package/src/cli/stop.ts +39 -0
- package/src/config/loader.ts +81 -0
- package/src/config/schema.ts +44 -0
- package/src/core/daemon.ts +213 -0
- package/src/core/heartbeat.ts +54 -0
- package/src/core/injector.ts +128 -0
- package/src/core/registry.ts +105 -0
- package/src/core/tmux.ts +139 -0
- package/src/core/websocket.ts +94 -0
- package/src/index.ts +9 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
export class AgentWebSocket {
|
|
3
|
+
ws = null;
|
|
4
|
+
config;
|
|
5
|
+
reconnectAttempts = 0;
|
|
6
|
+
maxReconnectAttempts = 10;
|
|
7
|
+
reconnectDelay = 1000;
|
|
8
|
+
shouldReconnect = true;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
}
|
|
12
|
+
connect() {
|
|
13
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const wsUrl = `${this.config.url}?token=${this.config.token}`;
|
|
17
|
+
this.ws = new WebSocket(wsUrl);
|
|
18
|
+
this.ws.on("open", () => {
|
|
19
|
+
this.reconnectAttempts = 0;
|
|
20
|
+
this.config.onConnect?.();
|
|
21
|
+
});
|
|
22
|
+
this.ws.on("message", (data) => {
|
|
23
|
+
try {
|
|
24
|
+
const event = JSON.parse(data.toString());
|
|
25
|
+
this.config.onMessage(event);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
this.ws.on("close", () => {
|
|
32
|
+
this.config.onDisconnect?.();
|
|
33
|
+
this.scheduleReconnect();
|
|
34
|
+
});
|
|
35
|
+
this.ws.on("error", (error) => {
|
|
36
|
+
this.config.onError?.(error);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
scheduleReconnect() {
|
|
40
|
+
if (!this.shouldReconnect) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
44
|
+
console.error("Max reconnect attempts reached");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.reconnectAttempts++;
|
|
48
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
console.log(`Reconnecting... (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
51
|
+
this.connect();
|
|
52
|
+
}, delay);
|
|
53
|
+
}
|
|
54
|
+
disconnect() {
|
|
55
|
+
this.shouldReconnect = false;
|
|
56
|
+
if (this.ws) {
|
|
57
|
+
this.ws.close();
|
|
58
|
+
this.ws = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
isConnected() {
|
|
62
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=websocket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/core/websocket.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { AgentDaemon } from "./core/daemon.js";
|
|
2
|
+
export { AgentWebSocket } from "./core/websocket.js";
|
|
3
|
+
export { Heartbeat } from "./core/heartbeat.js";
|
|
4
|
+
export * from "./core/tmux.js";
|
|
5
|
+
export * from "./core/registry.js";
|
|
6
|
+
export * from "./core/injector.js";
|
|
7
|
+
export * from "./config/schema.js";
|
|
8
|
+
export * from "./config/loader.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Re-export core modules for programmatic usage
|
|
2
|
+
export { AgentDaemon } from "./core/daemon.js";
|
|
3
|
+
export { AgentWebSocket } from "./core/websocket.js";
|
|
4
|
+
export { Heartbeat } from "./core/heartbeat.js";
|
|
5
|
+
export * from "./core/tmux.js";
|
|
6
|
+
export * from "./core/registry.js";
|
|
7
|
+
export * from "./core/injector.js";
|
|
8
|
+
export * from "./config/schema.js";
|
|
9
|
+
export * from "./config/loader.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentmeshhq/agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AgentMesh Agent Wrapper - Turn any AI coding assistant into a dispatchable, nudge-able agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agentmesh": "dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx watch src/cli/index.ts",
|
|
12
|
+
"build": "tsc -p tsconfig.json",
|
|
13
|
+
"lint": "biome check src",
|
|
14
|
+
"start": "node dist/cli/index.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@agentmesh/sdk": "workspace:*",
|
|
18
|
+
"commander": "^12.0.0",
|
|
19
|
+
"picocolors": "^1.0.0",
|
|
20
|
+
"ws": "^8.16.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.15.21",
|
|
24
|
+
"@types/ws": "^8.5.10"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"agentmesh",
|
|
28
|
+
"ai-agent",
|
|
29
|
+
"tmux",
|
|
30
|
+
"claude",
|
|
31
|
+
"opencode"
|
|
32
|
+
],
|
|
33
|
+
"author": "AgentMesh",
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { attachSession, sessionExists, getSessionName } from "../core/tmux.js";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
|
|
4
|
+
export function attach(name: string): void {
|
|
5
|
+
if (!name) {
|
|
6
|
+
console.log(pc.red("Agent name is required."));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sessionName = getSessionName(name);
|
|
11
|
+
|
|
12
|
+
if (!sessionExists(sessionName)) {
|
|
13
|
+
console.log(pc.red(`Agent "${name}" is not running.`));
|
|
14
|
+
console.log(`Start it with: ${pc.cyan(`agentmesh start --name ${name}`)}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(`Attaching to ${sessionName}...`);
|
|
19
|
+
console.log(pc.dim("Detach with: Ctrl+B, D\n"));
|
|
20
|
+
|
|
21
|
+
attachSession(name);
|
|
22
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { init } from "./init.js";
|
|
5
|
+
import { start } from "./start.js";
|
|
6
|
+
import { stop } from "./stop.js";
|
|
7
|
+
import { list } from "./list.js";
|
|
8
|
+
import { attach } from "./attach.js";
|
|
9
|
+
import { nudge } from "./nudge.js";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name("agentmesh")
|
|
16
|
+
.description("AgentMesh Agent Wrapper - Turn any AI assistant into a dispatchable agent")
|
|
17
|
+
.version("0.1.0");
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command("init")
|
|
21
|
+
.description("Initialize AgentMesh configuration")
|
|
22
|
+
.action(async () => {
|
|
23
|
+
try {
|
|
24
|
+
await init();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(pc.red((error as Error).message));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command("start")
|
|
33
|
+
.description("Start an agent")
|
|
34
|
+
.requiredOption("-n, --name <name>", "Agent name")
|
|
35
|
+
.option("-c, --command <command>", "Command to run (default: opencode)")
|
|
36
|
+
.option("-w, --workdir <path>", "Working directory")
|
|
37
|
+
.option("-m, --model <model>", "Model identifier")
|
|
38
|
+
.option("-d, --daemonize", "Run in background")
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
try {
|
|
41
|
+
await start(options);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(pc.red((error as Error).message));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command("stop")
|
|
50
|
+
.description("Stop an agent")
|
|
51
|
+
.argument("<name>", "Agent name")
|
|
52
|
+
.action(async (name) => {
|
|
53
|
+
try {
|
|
54
|
+
await stop(name);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(pc.red((error as Error).message));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command("list")
|
|
63
|
+
.alias("ls")
|
|
64
|
+
.description("List running agents")
|
|
65
|
+
.action(async () => {
|
|
66
|
+
try {
|
|
67
|
+
await list();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(pc.red((error as Error).message));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
program
|
|
75
|
+
.command("attach")
|
|
76
|
+
.description("Attach to an agent's tmux session")
|
|
77
|
+
.argument("<name>", "Agent name")
|
|
78
|
+
.action((name) => {
|
|
79
|
+
try {
|
|
80
|
+
attach(name);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(pc.red((error as Error).message));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
program
|
|
88
|
+
.command("nudge")
|
|
89
|
+
.description("Send a nudge to an agent")
|
|
90
|
+
.argument("<name>", "Agent name")
|
|
91
|
+
.argument("<message>", "Message to send")
|
|
92
|
+
.action(async (name, message) => {
|
|
93
|
+
try {
|
|
94
|
+
await nudge(name, message);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(pc.red((error as Error).message));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
program.parse();
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import { loadConfig, saveConfig, createDefaultConfig } from "../config/loader.js";
|
|
3
|
+
import type { Config } from "../config/schema.js";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
|
|
6
|
+
function question(rl: readline.Interface, prompt: string): Promise<string> {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
rl.question(prompt, resolve);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function init(): Promise<void> {
|
|
13
|
+
const existingConfig = loadConfig();
|
|
14
|
+
|
|
15
|
+
if (existingConfig) {
|
|
16
|
+
console.log(pc.yellow("Config already exists at ~/.agentmesh/config.json"));
|
|
17
|
+
console.log("Current workspace:", pc.cyan(existingConfig.workspace));
|
|
18
|
+
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const overwrite = await question(rl, "Overwrite? (y/N): ");
|
|
25
|
+
rl.close();
|
|
26
|
+
|
|
27
|
+
if (overwrite.toLowerCase() !== "y") {
|
|
28
|
+
console.log("Aborted.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(pc.bold("\nAgentMesh Agent Setup\n"));
|
|
34
|
+
|
|
35
|
+
const rl = readline.createInterface({
|
|
36
|
+
input: process.stdin,
|
|
37
|
+
output: process.stdout,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const apiKey = await question(
|
|
42
|
+
rl,
|
|
43
|
+
`API Key ${pc.dim("(from agentmeshhq.dev/settings/api-keys)")}: `
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (!apiKey) {
|
|
47
|
+
console.log(pc.red("API Key is required."));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const workspace = await question(
|
|
52
|
+
rl,
|
|
53
|
+
`Workspace ${pc.dim("(default: agentmesh)")}: `
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const command = await question(
|
|
57
|
+
rl,
|
|
58
|
+
`Default command ${pc.dim("(default: opencode)")}: `
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const model = await question(
|
|
62
|
+
rl,
|
|
63
|
+
`Default model ${pc.dim("(default: claude-sonnet-4)")}: `
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const config: Config = createDefaultConfig(
|
|
67
|
+
apiKey.trim(),
|
|
68
|
+
workspace.trim() || "agentmesh"
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (command.trim()) {
|
|
72
|
+
config.defaults.command = command.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (model.trim()) {
|
|
76
|
+
config.defaults.model = model.trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
saveConfig(config);
|
|
80
|
+
|
|
81
|
+
console.log(pc.green("\nConfig saved to ~/.agentmesh/config.json"));
|
|
82
|
+
console.log("\nNext steps:");
|
|
83
|
+
console.log(` ${pc.cyan("agentmesh start --name my-agent")}`);
|
|
84
|
+
} finally {
|
|
85
|
+
rl.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/cli/list.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { loadState, loadConfig } from "../config/loader.js";
|
|
2
|
+
import { sessionExists, getSessionName, getSessionInfo } from "../core/tmux.js";
|
|
3
|
+
import { checkInbox } from "../core/registry.js";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
|
|
6
|
+
export async function list(): Promise<void> {
|
|
7
|
+
const state = loadState();
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
|
|
10
|
+
if (state.agents.length === 0) {
|
|
11
|
+
console.log(pc.dim("No agents running."));
|
|
12
|
+
console.log(`Start one with: ${pc.cyan("agentmesh start --name <name>")}`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(pc.bold("Running Agents:\n"));
|
|
17
|
+
console.log(
|
|
18
|
+
`${"NAME".padEnd(20)} ${"STATUS".padEnd(10)} ${"SESSION".padEnd(25)} ${"PENDING"}`
|
|
19
|
+
);
|
|
20
|
+
console.log("-".repeat(70));
|
|
21
|
+
|
|
22
|
+
for (const agent of state.agents) {
|
|
23
|
+
const sessionName = getSessionName(agent.name);
|
|
24
|
+
const exists = sessionExists(sessionName);
|
|
25
|
+
const info = getSessionInfo(agent.name);
|
|
26
|
+
|
|
27
|
+
let status = pc.green("online");
|
|
28
|
+
if (!exists) {
|
|
29
|
+
status = pc.red("offline");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let pending = pc.dim("-");
|
|
33
|
+
|
|
34
|
+
// Try to check inbox if we have a token
|
|
35
|
+
if (config && agent.token) {
|
|
36
|
+
try {
|
|
37
|
+
const items = await checkInbox(
|
|
38
|
+
config.hubUrl,
|
|
39
|
+
config.workspace,
|
|
40
|
+
agent.token
|
|
41
|
+
);
|
|
42
|
+
if (items.length > 0) {
|
|
43
|
+
pending = pc.yellow(`${items.length} handoff${items.length === 1 ? "" : "s"}`);
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Ignore errors
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const command = info.command ? pc.dim(`(${info.command})`) : "";
|
|
51
|
+
|
|
52
|
+
console.log(
|
|
53
|
+
`${agent.name.padEnd(20)} ${status.padEnd(19)} ${sessionName.padEnd(25)} ${pending}`
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (command) {
|
|
57
|
+
console.log(`${"".padEnd(20)} ${command}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log("");
|
|
62
|
+
}
|
package/src/cli/nudge.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { loadConfig, getAgentState, loadState } from "../config/loader.js";
|
|
2
|
+
import { sendKeys, sessionExists, getSessionName } from "../core/tmux.js";
|
|
3
|
+
import { registerAgent } from "../core/registry.js";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
|
|
6
|
+
export async function nudge(name: string, message: string): Promise<void> {
|
|
7
|
+
const config = loadConfig();
|
|
8
|
+
|
|
9
|
+
if (!config) {
|
|
10
|
+
console.log(pc.red("No config found. Run 'agentmesh init' first."));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!name) {
|
|
15
|
+
console.log(pc.red("Agent name is required."));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!message) {
|
|
20
|
+
console.log(pc.red("Message is required."));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if this is a local agent
|
|
25
|
+
const localAgent = getAgentState(name);
|
|
26
|
+
|
|
27
|
+
if (localAgent && sessionExists(getSessionName(name))) {
|
|
28
|
+
// Local nudge via tmux send-keys
|
|
29
|
+
const formatted = `[AgentMesh] Nudge from CLI:
|
|
30
|
+
${message}`;
|
|
31
|
+
|
|
32
|
+
const sent = sendKeys(name, formatted);
|
|
33
|
+
|
|
34
|
+
if (sent) {
|
|
35
|
+
console.log(pc.green(`Nudged "${name}" locally.`));
|
|
36
|
+
} else {
|
|
37
|
+
console.log(pc.red(`Failed to nudge "${name}".`));
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Remote nudge via API
|
|
43
|
+
// First we need to find the agent ID
|
|
44
|
+
const state = loadState();
|
|
45
|
+
const agentState = state.agents.find((a) => a.name === name);
|
|
46
|
+
|
|
47
|
+
if (!agentState?.agentId) {
|
|
48
|
+
console.log(pc.red(`Agent "${name}" not found.`));
|
|
49
|
+
console.log(pc.dim("For remote nudges, provide the agent ID instead."));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Register ourselves to get a token
|
|
54
|
+
try {
|
|
55
|
+
const registration = await registerAgent({
|
|
56
|
+
url: config.hubUrl,
|
|
57
|
+
apiKey: config.apiKey,
|
|
58
|
+
workspace: config.workspace,
|
|
59
|
+
agentName: "cli-nudger",
|
|
60
|
+
model: "cli",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const response = await fetch(
|
|
64
|
+
`${config.hubUrl}/api/v1/agents/${agentState.agentId}/nudge`,
|
|
65
|
+
{
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${registration.token}`,
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({ message }),
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (response.ok) {
|
|
76
|
+
console.log(pc.green(`Nudged "${name}" via API.`));
|
|
77
|
+
} else {
|
|
78
|
+
const error = await response.text();
|
|
79
|
+
console.log(pc.red(`Failed to nudge: ${error}`));
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.log(pc.red(`Failed to nudge: ${(error as Error).message}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/cli/start.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AgentDaemon } from "../core/daemon.js";
|
|
2
|
+
import { loadConfig, getAgentState } from "../config/loader.js";
|
|
3
|
+
import { sessionExists, getSessionName } from "../core/tmux.js";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
|
|
6
|
+
export interface StartOptions {
|
|
7
|
+
name: string;
|
|
8
|
+
command?: string;
|
|
9
|
+
workdir?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
daemonize?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function start(options: StartOptions): Promise<void> {
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
|
|
17
|
+
if (!config) {
|
|
18
|
+
console.log(pc.red("No config found. Run 'agentmesh init' first."));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!options.name) {
|
|
23
|
+
console.log(pc.red("Agent name is required. Use --name <name>"));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check if already running
|
|
28
|
+
const existingState = getAgentState(options.name);
|
|
29
|
+
const sessionName = getSessionName(options.name);
|
|
30
|
+
|
|
31
|
+
if (existingState && sessionExists(sessionName)) {
|
|
32
|
+
console.log(pc.yellow(`Agent "${options.name}" is already running.`));
|
|
33
|
+
console.log(`Attach with: ${pc.cyan(`agentmesh attach ${options.name}`)}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const daemon = new AgentDaemon(options);
|
|
39
|
+
await daemon.start();
|
|
40
|
+
|
|
41
|
+
// Keep process alive in foreground mode
|
|
42
|
+
if (!options.daemonize) {
|
|
43
|
+
// Process will stay alive due to intervals and WebSocket
|
|
44
|
+
await new Promise(() => {}); // Never resolves
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(pc.red(`Failed to start agent: ${(error as Error).message}`));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/cli/stop.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { destroySession } from "../core/tmux.js";
|
|
2
|
+
import { removeAgentFromState, getAgentState } from "../config/loader.js";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
|
|
5
|
+
export async function stop(name: string): Promise<void> {
|
|
6
|
+
if (!name) {
|
|
7
|
+
console.log(pc.red("Agent name is required."));
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const state = getAgentState(name);
|
|
12
|
+
|
|
13
|
+
if (!state) {
|
|
14
|
+
console.log(pc.yellow(`Agent "${name}" is not running.`));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Try to kill the daemon process
|
|
19
|
+
if (state.pid) {
|
|
20
|
+
try {
|
|
21
|
+
process.kill(state.pid, "SIGTERM");
|
|
22
|
+
console.log(`Sent SIGTERM to daemon process ${state.pid}`);
|
|
23
|
+
} catch {
|
|
24
|
+
// Process might already be dead
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Destroy tmux session
|
|
29
|
+
const destroyed = destroySession(name);
|
|
30
|
+
|
|
31
|
+
if (destroyed) {
|
|
32
|
+
console.log(pc.green(`Destroyed tmux session for "${name}"`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Remove from state
|
|
36
|
+
removeAgentFromState(name);
|
|
37
|
+
|
|
38
|
+
console.log(pc.green(`Agent "${name}" stopped.`));
|
|
39
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type Config,
|
|
5
|
+
type State,
|
|
6
|
+
type AgentState,
|
|
7
|
+
CONFIG_PATH,
|
|
8
|
+
STATE_PATH,
|
|
9
|
+
DEFAULT_CONFIG,
|
|
10
|
+
} from "./schema.js";
|
|
11
|
+
|
|
12
|
+
export function ensureConfigDir(): void {
|
|
13
|
+
const configDir = path.dirname(CONFIG_PATH);
|
|
14
|
+
if (!fs.existsSync(configDir)) {
|
|
15
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadConfig(): Config | null {
|
|
20
|
+
try {
|
|
21
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const content = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
25
|
+
return JSON.parse(content) as Config;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveConfig(config: Config): void {
|
|
32
|
+
ensureConfigDir();
|
|
33
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadState(): State {
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(STATE_PATH)) {
|
|
39
|
+
return { agents: [] };
|
|
40
|
+
}
|
|
41
|
+
const content = fs.readFileSync(STATE_PATH, "utf-8");
|
|
42
|
+
return JSON.parse(content) as State;
|
|
43
|
+
} catch {
|
|
44
|
+
return { agents: [] };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function saveState(state: State): void {
|
|
49
|
+
ensureConfigDir();
|
|
50
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function addAgentToState(agent: AgentState): void {
|
|
54
|
+
const state = loadState();
|
|
55
|
+
// Remove existing entry with same name
|
|
56
|
+
state.agents = state.agents.filter((a) => a.name !== agent.name);
|
|
57
|
+
state.agents.push(agent);
|
|
58
|
+
saveState(state);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function removeAgentFromState(name: string): void {
|
|
62
|
+
const state = loadState();
|
|
63
|
+
state.agents = state.agents.filter((a) => a.name !== name);
|
|
64
|
+
saveState(state);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getAgentState(name: string): AgentState | undefined {
|
|
68
|
+
const state = loadState();
|
|
69
|
+
return state.agents.find((a) => a.name === name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createDefaultConfig(
|
|
73
|
+
apiKey: string,
|
|
74
|
+
workspace: string
|
|
75
|
+
): Config {
|
|
76
|
+
return {
|
|
77
|
+
...DEFAULT_CONFIG,
|
|
78
|
+
apiKey,
|
|
79
|
+
workspace,
|
|
80
|
+
} as Config;
|
|
81
|
+
}
|