@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.
Files changed (67) hide show
  1. package/README.md +111 -0
  2. package/dist/cli/attach.d.ts +1 -0
  3. package/dist/cli/attach.js +18 -0
  4. package/dist/cli/attach.js.map +1 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +98 -0
  7. package/dist/cli/index.js.map +1 -0
  8. package/dist/cli/init.d.ts +1 -0
  9. package/dist/cli/init.js +55 -0
  10. package/dist/cli/init.js.map +1 -0
  11. package/dist/cli/list.d.ts +1 -0
  12. package/dist/cli/list.js +45 -0
  13. package/dist/cli/list.js.map +1 -0
  14. package/dist/cli/nudge.d.ts +1 -0
  15. package/dist/cli/nudge.js +72 -0
  16. package/dist/cli/nudge.js.map +1 -0
  17. package/dist/cli/start.d.ts +8 -0
  18. package/dist/cli/start.js +37 -0
  19. package/dist/cli/start.js.map +1 -0
  20. package/dist/cli/stop.d.ts +1 -0
  21. package/dist/cli/stop.js +33 -0
  22. package/dist/cli/stop.js.map +1 -0
  23. package/dist/config/loader.d.ts +10 -0
  24. package/dist/config/loader.js +65 -0
  25. package/dist/config/loader.js.map +1 -0
  26. package/dist/config/schema.d.ts +32 -0
  27. package/dist/config/schema.js +11 -0
  28. package/dist/config/schema.js.map +1 -0
  29. package/dist/core/daemon.d.ts +20 -0
  30. package/dist/core/daemon.js +164 -0
  31. package/dist/core/daemon.js.map +1 -0
  32. package/dist/core/heartbeat.d.ts +14 -0
  33. package/dist/core/heartbeat.js +42 -0
  34. package/dist/core/heartbeat.js.map +1 -0
  35. package/dist/core/injector.d.ts +8 -0
  36. package/dist/core/injector.js +84 -0
  37. package/dist/core/injector.js.map +1 -0
  38. package/dist/core/registry.d.ts +27 -0
  39. package/dist/core/registry.js +52 -0
  40. package/dist/core/registry.js.map +1 -0
  41. package/dist/core/tmux.d.ts +11 -0
  42. package/dist/core/tmux.js +112 -0
  43. package/dist/core/tmux.js.map +1 -0
  44. package/dist/core/websocket.d.ts +25 -0
  45. package/dist/core/websocket.js +65 -0
  46. package/dist/core/websocket.js.map +1 -0
  47. package/dist/index.d.ts +8 -0
  48. package/dist/index.js +10 -0
  49. package/dist/index.js.map +1 -0
  50. package/package.json +35 -0
  51. package/src/cli/attach.ts +22 -0
  52. package/src/cli/index.ts +101 -0
  53. package/src/cli/init.ts +87 -0
  54. package/src/cli/list.ts +62 -0
  55. package/src/cli/nudge.ts +84 -0
  56. package/src/cli/start.ts +50 -0
  57. package/src/cli/stop.ts +39 -0
  58. package/src/config/loader.ts +81 -0
  59. package/src/config/schema.ts +44 -0
  60. package/src/core/daemon.ts +213 -0
  61. package/src/core/heartbeat.ts +54 -0
  62. package/src/core/injector.ts +128 -0
  63. package/src/core/registry.ts +105 -0
  64. package/src/core/tmux.ts +139 -0
  65. package/src/core/websocket.ts +94 -0
  66. package/src/index.ts +9 -0
  67. 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
+ }
@@ -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
+ }