@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,44 @@
|
|
|
1
|
+
export interface AgentConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
agentId?: string;
|
|
4
|
+
command: string;
|
|
5
|
+
workdir?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
teams?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Config {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
workspace: string;
|
|
13
|
+
hubUrl: string;
|
|
14
|
+
defaults: {
|
|
15
|
+
command: string;
|
|
16
|
+
model: string;
|
|
17
|
+
};
|
|
18
|
+
agents: AgentConfig[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_CONFIG: Partial<Config> = {
|
|
22
|
+
hubUrl: "https://agentmeshhq.dev",
|
|
23
|
+
defaults: {
|
|
24
|
+
command: "opencode",
|
|
25
|
+
model: "claude-sonnet-4",
|
|
26
|
+
},
|
|
27
|
+
agents: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const CONFIG_PATH = `${process.env.HOME}/.agentmesh/config.json`;
|
|
31
|
+
export const STATE_PATH = `${process.env.HOME}/.agentmesh/state.json`;
|
|
32
|
+
|
|
33
|
+
export interface AgentState {
|
|
34
|
+
name: string;
|
|
35
|
+
agentId: string;
|
|
36
|
+
pid: number;
|
|
37
|
+
tmuxSession: string;
|
|
38
|
+
startedAt: string;
|
|
39
|
+
token?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface State {
|
|
43
|
+
agents: AgentState[];
|
|
44
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createSession, destroySession, sessionExists, getSessionName } from "./tmux.js";
|
|
2
|
+
import { AgentWebSocket } from "./websocket.js";
|
|
3
|
+
import { Heartbeat } from "./heartbeat.js";
|
|
4
|
+
import { registerAgent, checkInbox } from "./registry.js";
|
|
5
|
+
import { injectStartupMessage, handleWebSocketEvent } from "./injector.js";
|
|
6
|
+
import {
|
|
7
|
+
addAgentToState,
|
|
8
|
+
removeAgentFromState,
|
|
9
|
+
loadConfig,
|
|
10
|
+
getAgentState,
|
|
11
|
+
} from "../config/loader.js";
|
|
12
|
+
import type { Config, AgentConfig } from "../config/schema.js";
|
|
13
|
+
|
|
14
|
+
export interface DaemonOptions {
|
|
15
|
+
name: string;
|
|
16
|
+
command?: string;
|
|
17
|
+
workdir?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
daemonize?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class AgentDaemon {
|
|
23
|
+
private agentName: string;
|
|
24
|
+
private config: Config;
|
|
25
|
+
private agentConfig: AgentConfig;
|
|
26
|
+
private ws: AgentWebSocket | null = null;
|
|
27
|
+
private heartbeat: Heartbeat | null = null;
|
|
28
|
+
private token: string | null = null;
|
|
29
|
+
private agentId: string | null = null;
|
|
30
|
+
private isRunning = false;
|
|
31
|
+
|
|
32
|
+
constructor(options: DaemonOptions) {
|
|
33
|
+
const config = loadConfig();
|
|
34
|
+
if (!config) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"No config found. Run 'agentmesh init' first."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.config = config;
|
|
41
|
+
this.agentName = options.name;
|
|
42
|
+
|
|
43
|
+
// Find or create agent config
|
|
44
|
+
let agentConfig = config.agents.find((a) => a.name === options.name);
|
|
45
|
+
|
|
46
|
+
if (!agentConfig) {
|
|
47
|
+
agentConfig = {
|
|
48
|
+
name: options.name,
|
|
49
|
+
command: options.command || config.defaults.command,
|
|
50
|
+
workdir: options.workdir,
|
|
51
|
+
model: options.model || config.defaults.model,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Override with provided options
|
|
56
|
+
if (options.command) agentConfig.command = options.command;
|
|
57
|
+
if (options.workdir) agentConfig.workdir = options.workdir;
|
|
58
|
+
if (options.model) agentConfig.model = options.model;
|
|
59
|
+
|
|
60
|
+
this.agentConfig = agentConfig;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async start(): Promise<void> {
|
|
64
|
+
if (this.isRunning) {
|
|
65
|
+
console.error("Daemon already running");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`Starting agent: ${this.agentName}`);
|
|
70
|
+
|
|
71
|
+
// Check if session already exists
|
|
72
|
+
const sessionName = getSessionName(this.agentName);
|
|
73
|
+
const sessionAlreadyExists = sessionExists(sessionName);
|
|
74
|
+
|
|
75
|
+
// Create tmux session if it doesn't exist
|
|
76
|
+
if (!sessionAlreadyExists) {
|
|
77
|
+
console.log(`Creating tmux session: ${sessionName}`);
|
|
78
|
+
const created = createSession(
|
|
79
|
+
this.agentName,
|
|
80
|
+
this.agentConfig.command,
|
|
81
|
+
this.agentConfig.workdir
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (!created) {
|
|
85
|
+
throw new Error("Failed to create tmux session");
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
console.log(`Reconnecting to existing session: ${sessionName}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Register with hub
|
|
92
|
+
console.log("Registering with AgentMesh hub...");
|
|
93
|
+
const existingState = getAgentState(this.agentName);
|
|
94
|
+
|
|
95
|
+
const registration = await registerAgent({
|
|
96
|
+
url: this.config.hubUrl,
|
|
97
|
+
apiKey: this.config.apiKey,
|
|
98
|
+
workspace: this.config.workspace,
|
|
99
|
+
agentId: existingState?.agentId || this.agentConfig.agentId,
|
|
100
|
+
agentName: this.agentName,
|
|
101
|
+
model: this.agentConfig.model || this.config.defaults.model,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.agentId = registration.agentId;
|
|
105
|
+
this.token = registration.token;
|
|
106
|
+
|
|
107
|
+
console.log(`Registered as: ${this.agentId}`);
|
|
108
|
+
|
|
109
|
+
// Save state
|
|
110
|
+
addAgentToState({
|
|
111
|
+
name: this.agentName,
|
|
112
|
+
agentId: this.agentId,
|
|
113
|
+
pid: process.pid,
|
|
114
|
+
tmuxSession: sessionName,
|
|
115
|
+
startedAt: new Date().toISOString(),
|
|
116
|
+
token: this.token,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Start heartbeat
|
|
120
|
+
console.log("Starting heartbeat...");
|
|
121
|
+
this.heartbeat = new Heartbeat({
|
|
122
|
+
url: this.config.hubUrl,
|
|
123
|
+
token: this.token,
|
|
124
|
+
intervalMs: 30000,
|
|
125
|
+
onError: (error) => {
|
|
126
|
+
console.error("Heartbeat error:", error.message);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
this.heartbeat.start();
|
|
130
|
+
|
|
131
|
+
// Connect WebSocket
|
|
132
|
+
console.log("Connecting WebSocket...");
|
|
133
|
+
const wsUrl = this.config.hubUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
134
|
+
|
|
135
|
+
this.ws = new AgentWebSocket({
|
|
136
|
+
url: `${wsUrl}/ws/v1`,
|
|
137
|
+
token: this.token,
|
|
138
|
+
onMessage: (event) => {
|
|
139
|
+
handleWebSocketEvent(this.agentName, event);
|
|
140
|
+
},
|
|
141
|
+
onConnect: () => {
|
|
142
|
+
console.log("WebSocket connected");
|
|
143
|
+
},
|
|
144
|
+
onDisconnect: () => {
|
|
145
|
+
console.log("WebSocket disconnected");
|
|
146
|
+
},
|
|
147
|
+
onError: (error) => {
|
|
148
|
+
console.error("WebSocket error:", error.message);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
this.ws.connect();
|
|
152
|
+
|
|
153
|
+
// Check inbox and auto-nudge
|
|
154
|
+
console.log("Checking inbox...");
|
|
155
|
+
try {
|
|
156
|
+
const inboxItems = await checkInbox(
|
|
157
|
+
this.config.hubUrl,
|
|
158
|
+
this.config.workspace,
|
|
159
|
+
this.token
|
|
160
|
+
);
|
|
161
|
+
injectStartupMessage(this.agentName, inboxItems.length);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error("Failed to check inbox:", error);
|
|
164
|
+
injectStartupMessage(this.agentName, 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.isRunning = true;
|
|
168
|
+
|
|
169
|
+
console.log(`
|
|
170
|
+
Agent "${this.agentName}" is running.
|
|
171
|
+
|
|
172
|
+
Attach to session:
|
|
173
|
+
agentmesh attach ${this.agentName}
|
|
174
|
+
|
|
175
|
+
Stop agent:
|
|
176
|
+
agentmesh stop ${this.agentName}
|
|
177
|
+
|
|
178
|
+
Nudge agent:
|
|
179
|
+
agentmesh nudge ${this.agentName} "Your message"
|
|
180
|
+
`);
|
|
181
|
+
|
|
182
|
+
// Handle shutdown
|
|
183
|
+
process.on("SIGINT", () => this.stop());
|
|
184
|
+
process.on("SIGTERM", () => this.stop());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async stop(): Promise<void> {
|
|
188
|
+
console.log(`\nStopping agent: ${this.agentName}`);
|
|
189
|
+
|
|
190
|
+
this.isRunning = false;
|
|
191
|
+
|
|
192
|
+
// Stop heartbeat
|
|
193
|
+
if (this.heartbeat) {
|
|
194
|
+
this.heartbeat.stop();
|
|
195
|
+
this.heartbeat = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Disconnect WebSocket
|
|
199
|
+
if (this.ws) {
|
|
200
|
+
this.ws.disconnect();
|
|
201
|
+
this.ws = null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Destroy tmux session
|
|
205
|
+
destroySession(this.agentName);
|
|
206
|
+
|
|
207
|
+
// Remove from state
|
|
208
|
+
removeAgentFromState(this.agentName);
|
|
209
|
+
|
|
210
|
+
console.log("Agent stopped.");
|
|
211
|
+
process.exit(0);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface HeartbeatConfig {
|
|
2
|
+
url: string;
|
|
3
|
+
token: string;
|
|
4
|
+
intervalMs: number;
|
|
5
|
+
onError?: (error: Error) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class Heartbeat {
|
|
9
|
+
private config: HeartbeatConfig;
|
|
10
|
+
private intervalId: NodeJS.Timeout | null = null;
|
|
11
|
+
|
|
12
|
+
constructor(config: HeartbeatConfig) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
start(): void {
|
|
17
|
+
if (this.intervalId) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Send initial heartbeat
|
|
22
|
+
this.sendHeartbeat();
|
|
23
|
+
|
|
24
|
+
// Schedule recurring heartbeats
|
|
25
|
+
this.intervalId = setInterval(() => {
|
|
26
|
+
this.sendHeartbeat();
|
|
27
|
+
}, this.config.intervalMs);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
stop(): void {
|
|
31
|
+
if (this.intervalId) {
|
|
32
|
+
clearInterval(this.intervalId);
|
|
33
|
+
this.intervalId = null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async sendHeartbeat(): Promise<void> {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(`${this.config.url}/api/v1/agents/heartbeat`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: `Bearer ${this.config.token}`,
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Heartbeat failed: ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
this.config.onError?.(error as Error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { sendKeys } from "./tmux.js";
|
|
2
|
+
import type { InboxItem } from "./registry.js";
|
|
3
|
+
import type { WebSocketEvent } from "./websocket.js";
|
|
4
|
+
|
|
5
|
+
export function injectStartupMessage(
|
|
6
|
+
agentName: string,
|
|
7
|
+
pendingCount: number
|
|
8
|
+
): void {
|
|
9
|
+
if (pendingCount === 0) {
|
|
10
|
+
const message = `[AgentMesh] Connected and ready. No pending items in inbox.`;
|
|
11
|
+
sendKeys(agentName, message);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const message = `[AgentMesh] Welcome back! You have ${pendingCount} pending handoff${pendingCount === 1 ? "" : "s"} in your inbox.
|
|
16
|
+
Use agentmesh_check_inbox to see them, or agentmesh_accept_handoff to start working.`;
|
|
17
|
+
|
|
18
|
+
sendKeys(agentName, message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function injectHandoffReceived(
|
|
22
|
+
agentName: string,
|
|
23
|
+
event: WebSocketEvent
|
|
24
|
+
): void {
|
|
25
|
+
const fromName =
|
|
26
|
+
(event.from_agent as { display_name?: string })?.display_name ||
|
|
27
|
+
(event.from_agent_id as string) ||
|
|
28
|
+
"Unknown";
|
|
29
|
+
const scope = (event.scope as string) || "No scope provided";
|
|
30
|
+
const reason = (event.reason as string) || "No reason provided";
|
|
31
|
+
const handoffId = (event.handoff_id as string) || (event.id as string) || "unknown";
|
|
32
|
+
|
|
33
|
+
const message = `[AgentMesh] New handoff from ${fromName}
|
|
34
|
+
|
|
35
|
+
Scope: ${scope}
|
|
36
|
+
Reason: ${reason}
|
|
37
|
+
Handoff ID: ${handoffId}
|
|
38
|
+
|
|
39
|
+
Accept this handoff and begin work.`;
|
|
40
|
+
|
|
41
|
+
sendKeys(agentName, message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function injectNudge(
|
|
45
|
+
agentName: string,
|
|
46
|
+
event: WebSocketEvent
|
|
47
|
+
): void {
|
|
48
|
+
const fromName =
|
|
49
|
+
(event.from as { name?: string })?.name ||
|
|
50
|
+
(event.from_name as string) ||
|
|
51
|
+
"Someone";
|
|
52
|
+
const message = (event.message as string) || "Check your inbox";
|
|
53
|
+
|
|
54
|
+
const formatted = `[AgentMesh] Nudge from ${fromName}:
|
|
55
|
+
${message}`;
|
|
56
|
+
|
|
57
|
+
sendKeys(agentName, formatted);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function injectBlockerResolved(
|
|
61
|
+
agentName: string,
|
|
62
|
+
event: WebSocketEvent
|
|
63
|
+
): void {
|
|
64
|
+
const description =
|
|
65
|
+
(event.description as string) || "A blocker has been resolved";
|
|
66
|
+
const resolvedBy =
|
|
67
|
+
(event.resolved_by as { display_name?: string })?.display_name ||
|
|
68
|
+
(event.resolved_by_name as string) ||
|
|
69
|
+
"Another agent";
|
|
70
|
+
|
|
71
|
+
const message = `[AgentMesh] Blocker resolved!
|
|
72
|
+
|
|
73
|
+
Blocker: ${description}
|
|
74
|
+
Resolved by: ${resolvedBy}
|
|
75
|
+
|
|
76
|
+
You can now proceed with your work.`;
|
|
77
|
+
|
|
78
|
+
sendKeys(agentName, message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function injectInboxItems(
|
|
82
|
+
agentName: string,
|
|
83
|
+
items: InboxItem[]
|
|
84
|
+
): void {
|
|
85
|
+
if (items.length === 0) {
|
|
86
|
+
sendKeys(agentName, "[AgentMesh] Your inbox is empty.");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let message = `[AgentMesh] You have ${items.length} pending item${items.length === 1 ? "" : "s"}:\n\n`;
|
|
91
|
+
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
const fromName = item.from_agent?.display_name || item.from_agent_id;
|
|
94
|
+
message += `- From: ${fromName}\n`;
|
|
95
|
+
message += ` Scope: ${item.scope}\n`;
|
|
96
|
+
message += ` ID: ${item.id}\n\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
message += "Use agentmesh_accept_handoff with the ID to start working.";
|
|
100
|
+
|
|
101
|
+
sendKeys(agentName, message);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function handleWebSocketEvent(
|
|
105
|
+
agentName: string,
|
|
106
|
+
event: WebSocketEvent
|
|
107
|
+
): void {
|
|
108
|
+
switch (event.type) {
|
|
109
|
+
case "handoff_received":
|
|
110
|
+
case "handoff.received":
|
|
111
|
+
injectHandoffReceived(agentName, event);
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case "nudge":
|
|
115
|
+
case "agent.nudge":
|
|
116
|
+
injectNudge(agentName, event);
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
case "blocker_resolved":
|
|
120
|
+
case "blocker.resolved":
|
|
121
|
+
injectBlockerResolved(agentName, event);
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
default:
|
|
125
|
+
// Unknown event type, ignore
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export interface RegisterOptions {
|
|
4
|
+
url: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
workspace: string;
|
|
7
|
+
agentId?: string;
|
|
8
|
+
agentName: string;
|
|
9
|
+
model: string;
|
|
10
|
+
capabilities?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RegisterResult {
|
|
14
|
+
agentId: string;
|
|
15
|
+
token: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InboxItem {
|
|
19
|
+
id: string;
|
|
20
|
+
from_agent_id: string;
|
|
21
|
+
from_agent?: {
|
|
22
|
+
display_name?: string;
|
|
23
|
+
};
|
|
24
|
+
scope: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
status: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function registerAgent(
|
|
31
|
+
options: RegisterOptions
|
|
32
|
+
): Promise<RegisterResult> {
|
|
33
|
+
const agentId = options.agentId || randomUUID();
|
|
34
|
+
|
|
35
|
+
const response = await fetch(`${options.url}/api/v1/agents/register`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
"x-agentmesh-secret": options.apiKey,
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
agent_id: agentId,
|
|
43
|
+
display_name: options.agentName,
|
|
44
|
+
model: options.model,
|
|
45
|
+
capabilities: options.capabilities || ["coding", "review", "debugging"],
|
|
46
|
+
workspace: options.workspace,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const error = await response.text();
|
|
52
|
+
throw new Error(`Failed to register agent: ${error}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
const token = data.data?.token || data.token;
|
|
57
|
+
|
|
58
|
+
if (!token) {
|
|
59
|
+
throw new Error("No token in registration response");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { agentId, token };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function checkInbox(
|
|
66
|
+
url: string,
|
|
67
|
+
workspace: string,
|
|
68
|
+
token: string
|
|
69
|
+
): Promise<InboxItem[]> {
|
|
70
|
+
const response = await fetch(
|
|
71
|
+
`${url}/api/v1/workspaces/${workspace}/inbox`,
|
|
72
|
+
{
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${token}`,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Failed to check inbox: ${response.status}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
return (data.data || []).filter(
|
|
85
|
+
(item: InboxItem) => item.status === "pending"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function sendNudge(
|
|
90
|
+
url: string,
|
|
91
|
+
agentId: string,
|
|
92
|
+
message: string,
|
|
93
|
+
token: string
|
|
94
|
+
): Promise<boolean> {
|
|
95
|
+
const response = await fetch(`${url}/api/v1/agents/${agentId}/nudge`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
Authorization: `Bearer ${token}`,
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify({ message }),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return response.ok;
|
|
105
|
+
}
|
package/src/core/tmux.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { execSync, spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const SESSION_PREFIX = "agentmesh-";
|
|
4
|
+
|
|
5
|
+
export function getSessionName(agentName: string): string {
|
|
6
|
+
return `${SESSION_PREFIX}${agentName}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function sessionExists(sessionName: string): boolean {
|
|
10
|
+
try {
|
|
11
|
+
execSync(`tmux has-session -t "${sessionName}" 2>/dev/null`);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createSession(
|
|
19
|
+
agentName: string,
|
|
20
|
+
command: string,
|
|
21
|
+
workdir?: string
|
|
22
|
+
): boolean {
|
|
23
|
+
const sessionName = getSessionName(agentName);
|
|
24
|
+
|
|
25
|
+
if (sessionExists(sessionName)) {
|
|
26
|
+
console.error(`Session ${sessionName} already exists`);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const args = ["new-session", "-d", "-s", sessionName];
|
|
32
|
+
|
|
33
|
+
if (workdir) {
|
|
34
|
+
args.push("-c", workdir);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
args.push(command);
|
|
38
|
+
|
|
39
|
+
execSync(`tmux ${args.join(" ")}`);
|
|
40
|
+
return true;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`Failed to create tmux session: ${error}`);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function destroySession(agentName: string): boolean {
|
|
48
|
+
const sessionName = getSessionName(agentName);
|
|
49
|
+
|
|
50
|
+
if (!sessionExists(sessionName)) {
|
|
51
|
+
return true; // Already gone
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
execSync(`tmux kill-session -t "${sessionName}"`);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`Failed to destroy tmux session: ${error}`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function sendKeys(agentName: string, message: string): boolean {
|
|
64
|
+
const sessionName = getSessionName(agentName);
|
|
65
|
+
|
|
66
|
+
if (!sessionExists(sessionName)) {
|
|
67
|
+
console.error(`Session ${sessionName} does not exist`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Escape special characters for tmux
|
|
73
|
+
const escapedMessage = message
|
|
74
|
+
.replace(/\\/g, "\\\\")
|
|
75
|
+
.replace(/"/g, '\\"')
|
|
76
|
+
.replace(/\$/g, "\\$")
|
|
77
|
+
.replace(/`/g, "\\`");
|
|
78
|
+
|
|
79
|
+
execSync(`tmux send-keys -t "${sessionName}" "${escapedMessage}" Enter`);
|
|
80
|
+
return true;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(`Failed to send keys: ${error}`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function attachSession(agentName: string): void {
|
|
88
|
+
const sessionName = getSessionName(agentName);
|
|
89
|
+
|
|
90
|
+
if (!sessionExists(sessionName)) {
|
|
91
|
+
console.error(`Session ${sessionName} does not exist`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Attach to the session (replaces current process)
|
|
96
|
+
const tmux = spawn("tmux", ["attach-session", "-t", sessionName], {
|
|
97
|
+
stdio: "inherit",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
tmux.on("exit", (code) => {
|
|
101
|
+
process.exit(code ?? 0);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function listSessions(): string[] {
|
|
106
|
+
try {
|
|
107
|
+
const output = execSync("tmux list-sessions -F '#{session_name}'", {
|
|
108
|
+
encoding: "utf-8",
|
|
109
|
+
});
|
|
110
|
+
return output
|
|
111
|
+
.trim()
|
|
112
|
+
.split("\n")
|
|
113
|
+
.filter((s) => s.startsWith(SESSION_PREFIX))
|
|
114
|
+
.map((s) => s.replace(SESSION_PREFIX, ""));
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getSessionInfo(
|
|
121
|
+
agentName: string
|
|
122
|
+
): { exists: boolean; command?: string } {
|
|
123
|
+
const sessionName = getSessionName(agentName);
|
|
124
|
+
|
|
125
|
+
if (!sessionExists(sessionName)) {
|
|
126
|
+
return { exists: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const command = execSync(
|
|
131
|
+
`tmux list-panes -t "${sessionName}" -F "#{pane_current_command}"`,
|
|
132
|
+
{ encoding: "utf-8" }
|
|
133
|
+
).trim();
|
|
134
|
+
|
|
135
|
+
return { exists: true, command };
|
|
136
|
+
} catch {
|
|
137
|
+
return { exists: true };
|
|
138
|
+
}
|
|
139
|
+
}
|