@agentmeshhq/agent 0.1.4 → 0.1.6

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 (40) hide show
  1. package/dist/__tests__/jwt.test.d.ts +1 -0
  2. package/dist/__tests__/jwt.test.js +83 -0
  3. package/dist/__tests__/jwt.test.js.map +1 -0
  4. package/dist/__tests__/loader.test.d.ts +1 -0
  5. package/dist/__tests__/loader.test.js +148 -0
  6. package/dist/__tests__/loader.test.js.map +1 -0
  7. package/dist/cli/index.js +37 -5
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/token.d.ts +1 -0
  10. package/dist/cli/token.js +146 -0
  11. package/dist/cli/token.js.map +1 -0
  12. package/dist/cli/whoami.d.ts +1 -0
  13. package/dist/cli/whoami.js +98 -0
  14. package/dist/cli/whoami.js.map +1 -0
  15. package/dist/config/loader.d.ts +2 -1
  16. package/dist/config/loader.js +13 -1
  17. package/dist/config/loader.js.map +1 -1
  18. package/dist/core/daemon.js +50 -6
  19. package/dist/core/daemon.js.map +1 -1
  20. package/dist/core/heartbeat.d.ts +8 -0
  21. package/dist/core/heartbeat.js +50 -1
  22. package/dist/core/heartbeat.js.map +1 -1
  23. package/dist/core/tmux.d.ts +7 -1
  24. package/dist/core/tmux.js +31 -2
  25. package/dist/core/tmux.js.map +1 -1
  26. package/dist/utils/jwt.d.ts +36 -0
  27. package/dist/utils/jwt.js +70 -0
  28. package/dist/utils/jwt.js.map +1 -0
  29. package/package.json +6 -3
  30. package/src/__tests__/jwt.test.ts +112 -0
  31. package/src/__tests__/loader.test.ts +191 -0
  32. package/src/cli/index.ts +38 -5
  33. package/src/cli/token.ts +188 -0
  34. package/src/cli/whoami.ts +113 -0
  35. package/src/config/loader.ts +20 -7
  36. package/src/core/daemon.ts +64 -18
  37. package/src/core/heartbeat.ts +62 -1
  38. package/src/core/tmux.ts +46 -9
  39. package/src/utils/jwt.ts +87 -0
  40. package/vitest.config.ts +12 -0
@@ -1,15 +1,22 @@
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
1
  import {
7
2
  addAgentToState,
8
- removeAgentFromState,
9
- loadConfig,
10
3
  getAgentState,
4
+ loadConfig,
5
+ removeAgentFromState,
6
+ updateAgentInState,
11
7
  } from "../config/loader.js";
12
- import type { Config, AgentConfig } from "../config/schema.js";
8
+ import type { AgentConfig, Config } from "../config/schema.js";
9
+ import { Heartbeat } from "./heartbeat.js";
10
+ import { handleWebSocketEvent, injectStartupMessage } from "./injector.js";
11
+ import { checkInbox, registerAgent } from "./registry.js";
12
+ import {
13
+ createSession,
14
+ destroySession,
15
+ getSessionName,
16
+ sessionExists,
17
+ updateSessionEnvironment,
18
+ } from "./tmux.js";
19
+ import { AgentWebSocket } from "./websocket.js";
13
20
 
14
21
  export interface DaemonOptions {
15
22
  name: string;
@@ -32,9 +39,7 @@ export class AgentDaemon {
32
39
  constructor(options: DaemonOptions) {
33
40
  const config = loadConfig();
34
41
  if (!config) {
35
- throw new Error(
36
- "No config found. Run 'agentmesh init' first."
37
- );
42
+ throw new Error("No config found. Run 'agentmesh init' first.");
38
43
  }
39
44
 
40
45
  this.config = config;
@@ -78,7 +83,7 @@ export class AgentDaemon {
78
83
  const created = createSession(
79
84
  this.agentName,
80
85
  this.agentConfig.command,
81
- this.agentConfig.workdir
86
+ this.agentConfig.workdir,
82
87
  );
83
88
 
84
89
  if (!created) {
@@ -106,6 +111,13 @@ export class AgentDaemon {
106
111
 
107
112
  console.log(`Registered as: ${this.agentId}`);
108
113
 
114
+ // Inject environment variables into tmux session
115
+ console.log("Injecting environment variables...");
116
+ updateSessionEnvironment(this.agentName, {
117
+ AGENT_TOKEN: this.token,
118
+ AGENTMESH_AGENT_ID: this.agentId,
119
+ });
120
+
109
121
  // Save state
110
122
  addAgentToState({
111
123
  name: this.agentName,
@@ -116,15 +128,53 @@ export class AgentDaemon {
116
128
  token: this.token,
117
129
  });
118
130
 
119
- // Start heartbeat
131
+ // Start heartbeat with auto-refresh
120
132
  console.log("Starting heartbeat...");
121
133
  this.heartbeat = new Heartbeat({
122
134
  url: this.config.hubUrl,
123
135
  token: this.token,
124
136
  intervalMs: 30000,
137
+ agentName: this.agentName,
138
+ agentId: this.agentId,
139
+ apiKey: this.config.apiKey,
140
+ workspace: this.config.workspace,
125
141
  onError: (error) => {
126
142
  console.error("Heartbeat error:", error.message);
127
143
  },
144
+ onTokenRefresh: (newToken) => {
145
+ this.token = newToken;
146
+ // Update state file
147
+ updateAgentInState(this.agentName, { token: newToken });
148
+ // Update tmux environment
149
+ updateSessionEnvironment(this.agentName, {
150
+ AGENT_TOKEN: newToken,
151
+ AGENTMESH_AGENT_ID: this.agentId!,
152
+ });
153
+ // Reconnect WebSocket with new token
154
+ if (this.ws) {
155
+ this.ws.disconnect();
156
+ const wsUrl = this.config.hubUrl
157
+ .replace("https://", "wss://")
158
+ .replace("http://", "ws://");
159
+ this.ws = new AgentWebSocket({
160
+ url: `${wsUrl}/ws/v1`,
161
+ token: newToken,
162
+ onMessage: (event) => {
163
+ handleWebSocketEvent(this.agentName, event);
164
+ },
165
+ onConnect: () => {
166
+ console.log("WebSocket reconnected with new token");
167
+ },
168
+ onDisconnect: () => {
169
+ console.log("WebSocket disconnected");
170
+ },
171
+ onError: (error) => {
172
+ console.error("WebSocket error:", error.message);
173
+ },
174
+ });
175
+ this.ws.connect();
176
+ }
177
+ },
128
178
  });
129
179
  this.heartbeat.start();
130
180
 
@@ -153,11 +203,7 @@ export class AgentDaemon {
153
203
  // Check inbox and auto-nudge
154
204
  console.log("Checking inbox...");
155
205
  try {
156
- const inboxItems = await checkInbox(
157
- this.config.hubUrl,
158
- this.config.workspace,
159
- this.token
160
- );
206
+ const inboxItems = await checkInbox(this.config.hubUrl, this.config.workspace, this.token);
161
207
  injectStartupMessage(this.agentName, inboxItems.length);
162
208
  } catch (error) {
163
209
  console.error("Failed to check inbox:", error);
@@ -1,16 +1,28 @@
1
+ import { getTokenTimeRemaining, isTokenExpiringSoon } from "../utils/jwt.js";
2
+
3
+ // Refresh token when less than 24 hours remain
4
+ const TOKEN_REFRESH_THRESHOLD_MS = 24 * 60 * 60 * 1000;
5
+
1
6
  export interface HeartbeatConfig {
2
7
  url: string;
3
8
  token: string;
4
9
  intervalMs: number;
10
+ agentName: string;
11
+ agentId: string;
12
+ apiKey: string;
13
+ workspace: string;
5
14
  onError?: (error: Error) => void;
15
+ onTokenRefresh?: (newToken: string) => void;
6
16
  }
7
17
 
8
18
  export class Heartbeat {
9
19
  private config: HeartbeatConfig;
20
+ private currentToken: string;
10
21
  private intervalId: NodeJS.Timeout | null = null;
11
22
 
12
23
  constructor(config: HeartbeatConfig) {
13
24
  this.config = config;
25
+ this.currentToken = config.token;
14
26
  }
15
27
 
16
28
  start(): void {
@@ -34,22 +46,71 @@ export class Heartbeat {
34
46
  }
35
47
  }
36
48
 
49
+ getToken(): string {
50
+ return this.currentToken;
51
+ }
52
+
37
53
  private async sendHeartbeat(): Promise<void> {
38
54
  try {
55
+ // Check if token needs refresh before sending heartbeat
56
+ if (isTokenExpiringSoon(this.currentToken, TOKEN_REFRESH_THRESHOLD_MS)) {
57
+ const remaining = getTokenTimeRemaining(this.currentToken);
58
+ const hours = Math.floor(remaining / (1000 * 60 * 60));
59
+ console.log(`Token expiring in ${hours} hours, refreshing...`);
60
+ await this.refreshToken();
61
+ }
62
+
39
63
  const response = await fetch(`${this.config.url}/api/v1/agents/heartbeat`, {
40
64
  method: "POST",
41
65
  headers: {
42
- Authorization: `Bearer ${this.config.token}`,
66
+ Authorization: `Bearer ${this.currentToken}`,
43
67
  "Content-Type": "application/json",
44
68
  },
45
69
  body: JSON.stringify({}),
46
70
  });
47
71
 
48
72
  if (!response.ok) {
73
+ // If unauthorized, try to refresh token
74
+ if (response.status === 401) {
75
+ console.log("Token rejected, attempting refresh...");
76
+ await this.refreshToken();
77
+ return;
78
+ }
49
79
  throw new Error(`Heartbeat failed: ${response.status}`);
50
80
  }
51
81
  } catch (error) {
52
82
  this.config.onError?.(error as Error);
53
83
  }
54
84
  }
85
+
86
+ private async refreshToken(): Promise<void> {
87
+ try {
88
+ const response = await fetch(`${this.config.url}/api/v1/agents/register`, {
89
+ method: "POST",
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ "x-agentmesh-secret": this.config.apiKey,
93
+ },
94
+ body: JSON.stringify({
95
+ agent_id: this.config.agentId,
96
+ workspace: this.config.workspace,
97
+ display_name: this.config.agentName,
98
+ model: "claude-sonnet-4",
99
+ }),
100
+ });
101
+
102
+ if (!response.ok) {
103
+ throw new Error(`Token refresh failed: ${response.status}`);
104
+ }
105
+
106
+ const data = (await response.json()) as { token: string };
107
+ this.currentToken = data.token;
108
+
109
+ console.log("Token refreshed successfully");
110
+ this.config.onTokenRefresh?.(data.token);
111
+ } catch (error) {
112
+ console.error("Failed to refresh token:", (error as Error).message);
113
+ this.config.onError?.(error as Error);
114
+ }
115
+ }
55
116
  }
package/src/core/tmux.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { execSync, spawn, type ChildProcess } from "node:child_process";
1
+ import { type ChildProcess, execSync, spawn } from "node:child_process";
2
2
 
3
3
  const SESSION_PREFIX = "agentmesh-";
4
4
 
@@ -15,10 +15,16 @@ export function sessionExists(sessionName: string): boolean {
15
15
  }
16
16
  }
17
17
 
18
+ export interface SessionEnv {
19
+ AGENT_TOKEN?: string;
20
+ AGENTMESH_AGENT_ID?: string;
21
+ }
22
+
18
23
  export function createSession(
19
24
  agentName: string,
20
25
  command: string,
21
- workdir?: string
26
+ workdir?: string,
27
+ env?: SessionEnv,
22
28
  ): boolean {
23
29
  const sessionName = getSessionName(agentName);
24
30
 
@@ -37,6 +43,12 @@ export function createSession(
37
43
  args.push(command);
38
44
 
39
45
  execSync(`tmux ${args.join(" ")}`);
46
+
47
+ // Inject environment variables after session creation
48
+ if (env) {
49
+ setSessionEnvironment(sessionName, env);
50
+ }
51
+
40
52
  return true;
41
53
  } catch (error) {
42
54
  console.error(`Failed to create tmux session: ${error}`);
@@ -44,6 +56,34 @@ export function createSession(
44
56
  }
45
57
  }
46
58
 
59
+ export function setSessionEnvironment(sessionName: string, env: SessionEnv): boolean {
60
+ try {
61
+ if (env.AGENT_TOKEN) {
62
+ execSync(`tmux set-environment -t "${sessionName}" AGENT_TOKEN "${env.AGENT_TOKEN}"`);
63
+ }
64
+ if (env.AGENTMESH_AGENT_ID) {
65
+ execSync(
66
+ `tmux set-environment -t "${sessionName}" AGENTMESH_AGENT_ID "${env.AGENTMESH_AGENT_ID}"`,
67
+ );
68
+ }
69
+ return true;
70
+ } catch (error) {
71
+ console.error(`Failed to set session environment: ${error}`);
72
+ return false;
73
+ }
74
+ }
75
+
76
+ export function updateSessionEnvironment(agentName: string, env: SessionEnv): boolean {
77
+ const sessionName = getSessionName(agentName);
78
+
79
+ if (!sessionExists(sessionName)) {
80
+ console.error(`Session ${sessionName} does not exist`);
81
+ return false;
82
+ }
83
+
84
+ return setSessionEnvironment(sessionName, env);
85
+ }
86
+
47
87
  export function destroySession(agentName: string): boolean {
48
88
  const sessionName = getSessionName(agentName);
49
89
 
@@ -119,9 +159,7 @@ export function listSessions(): string[] {
119
159
  }
120
160
  }
121
161
 
122
- export function getSessionInfo(
123
- agentName: string
124
- ): { exists: boolean; command?: string } {
162
+ export function getSessionInfo(agentName: string): { exists: boolean; command?: string } {
125
163
  const sessionName = getSessionName(agentName);
126
164
 
127
165
  if (!sessionExists(sessionName)) {
@@ -129,10 +167,9 @@ export function getSessionInfo(
129
167
  }
130
168
 
131
169
  try {
132
- const command = execSync(
133
- `tmux list-panes -t "${sessionName}" -F "#{pane_current_command}"`,
134
- { encoding: "utf-8" }
135
- ).trim();
170
+ const command = execSync(`tmux list-panes -t "${sessionName}" -F "#{pane_current_command}"`, {
171
+ encoding: "utf-8",
172
+ }).trim();
136
173
 
137
174
  return { exists: true, command };
138
175
  } catch {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * JWT utilities for token management
3
+ * Note: We only decode the payload, we don't verify signatures here
4
+ * (verification happens on the hub side)
5
+ */
6
+
7
+ export interface TokenPayload {
8
+ sub: string; // Agent ID
9
+ actorType: string; // "agent"
10
+ workspaceScopes: string[];
11
+ iat: number; // Issued at (Unix timestamp)
12
+ exp: number; // Expiration (Unix timestamp)
13
+ }
14
+
15
+ /**
16
+ * Decode a JWT token without verifying the signature
17
+ */
18
+ export function decodeToken(token: string): TokenPayload | null {
19
+ try {
20
+ const parts = token.split(".");
21
+ if (parts.length !== 3) {
22
+ return null;
23
+ }
24
+
25
+ const payload = parts[1];
26
+ const decoded = Buffer.from(payload, "base64url").toString("utf-8");
27
+ return JSON.parse(decoded) as TokenPayload;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Get the expiration date from a token
35
+ */
36
+ export function getTokenExpiry(token: string): Date | null {
37
+ const payload = decodeToken(token);
38
+ if (!payload || !payload.exp) {
39
+ return null;
40
+ }
41
+
42
+ return new Date(payload.exp * 1000);
43
+ }
44
+
45
+ /**
46
+ * Check if a token is expired
47
+ */
48
+ export function isTokenExpired(token: string): boolean {
49
+ const expiry = getTokenExpiry(token);
50
+ if (!expiry) {
51
+ return true;
52
+ }
53
+
54
+ return expiry.getTime() <= Date.now();
55
+ }
56
+
57
+ /**
58
+ * Check if a token will expire within the given milliseconds
59
+ */
60
+ export function isTokenExpiringSoon(token: string, withinMs: number): boolean {
61
+ const expiry = getTokenExpiry(token);
62
+ if (!expiry) {
63
+ return true;
64
+ }
65
+
66
+ return expiry.getTime() <= Date.now() + withinMs;
67
+ }
68
+
69
+ /**
70
+ * Get the agent ID from a token
71
+ */
72
+ export function getAgentIdFromToken(token: string): string | null {
73
+ const payload = decodeToken(token);
74
+ return payload?.sub ?? null;
75
+ }
76
+
77
+ /**
78
+ * Get time remaining until token expires in milliseconds
79
+ */
80
+ export function getTokenTimeRemaining(token: string): number {
81
+ const expiry = getTokenExpiry(token);
82
+ if (!expiry) {
83
+ return 0;
84
+ }
85
+
86
+ return Math.max(0, expiry.getTime() - Date.now());
87
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ include: ["src/__tests__/**/*.test.ts"],
8
+ coverage: {
9
+ reporter: ["text", "json", "html"],
10
+ },
11
+ },
12
+ });