@agentmeshhq/agent 0.1.10 → 0.1.12

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 (52) hide show
  1. package/dist/__tests__/injector.test.d.ts +1 -0
  2. package/dist/__tests__/injector.test.js +26 -0
  3. package/dist/__tests__/injector.test.js.map +1 -0
  4. package/dist/cli/build.d.ts +6 -0
  5. package/dist/cli/build.js +111 -0
  6. package/dist/cli/build.js.map +1 -0
  7. package/dist/cli/deploy.d.ts +9 -0
  8. package/dist/cli/deploy.js +130 -0
  9. package/dist/cli/deploy.js.map +1 -0
  10. package/dist/cli/index.js +155 -0
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/list.js +18 -6
  13. package/dist/cli/list.js.map +1 -1
  14. package/dist/cli/local.d.ts +9 -0
  15. package/dist/cli/local.js +139 -0
  16. package/dist/cli/local.js.map +1 -0
  17. package/dist/cli/migrate.d.ts +8 -0
  18. package/dist/cli/migrate.js +167 -0
  19. package/dist/cli/migrate.js.map +1 -0
  20. package/dist/cli/slack.d.ts +3 -0
  21. package/dist/cli/slack.js +57 -0
  22. package/dist/cli/slack.js.map +1 -0
  23. package/dist/cli/start.d.ts +4 -0
  24. package/dist/cli/start.js +5 -0
  25. package/dist/cli/start.js.map +1 -1
  26. package/dist/cli/test.d.ts +8 -0
  27. package/dist/cli/test.js +110 -0
  28. package/dist/cli/test.js.map +1 -0
  29. package/dist/config/schema.d.ts +2 -0
  30. package/dist/core/daemon.d.ts +13 -0
  31. package/dist/core/daemon.js +100 -27
  32. package/dist/core/daemon.js.map +1 -1
  33. package/dist/core/injector.d.ts +6 -1
  34. package/dist/core/injector.js +64 -1
  35. package/dist/core/injector.js.map +1 -1
  36. package/dist/core/tmux.js +11 -13
  37. package/dist/core/tmux.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/__tests__/injector.test.ts +29 -0
  40. package/src/cli/build.ts +137 -0
  41. package/src/cli/deploy.ts +153 -0
  42. package/src/cli/index.ts +156 -0
  43. package/src/cli/list.ts +18 -6
  44. package/src/cli/local.ts +174 -0
  45. package/src/cli/migrate.ts +210 -0
  46. package/src/cli/slack.ts +69 -0
  47. package/src/cli/start.ts +8 -0
  48. package/src/cli/test.ts +141 -0
  49. package/src/config/schema.ts +2 -0
  50. package/src/core/daemon.ts +123 -35
  51. package/src/core/injector.ts +98 -1
  52. package/src/core/tmux.ts +12 -14
@@ -1,4 +1,4 @@
1
- import { execSync } from "node:child_process";
1
+ import { type ChildProcess, execSync, spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -40,6 +40,10 @@ export interface DaemonOptions {
40
40
  restoreContext?: boolean;
41
41
  /** Auto-clone repository for project assignments */
42
42
  autoSetup?: boolean;
43
+ /** Run opencode serve instead of tmux TUI (for Integration Service) */
44
+ serve?: boolean;
45
+ /** Port for opencode serve (default: 3001) */
46
+ servePort?: number;
43
47
  }
44
48
 
45
49
  export class AgentDaemon {
@@ -52,8 +56,12 @@ export class AgentDaemon {
52
56
  private token: string | null = null;
53
57
  private agentId: string | null = null;
54
58
  private isRunning = false;
59
+ private assignedProject: string | undefined;
55
60
  private shouldRestoreContext: boolean;
56
61
  private autoSetup: boolean;
62
+ private serveMode: boolean;
63
+ private servePort: number;
64
+ private serveProcess: ChildProcess | null = null;
57
65
 
58
66
  constructor(options: DaemonOptions) {
59
67
  const config = loadConfig();
@@ -84,6 +92,8 @@ export class AgentDaemon {
84
92
  if (options.model) agentConfig.model = options.model;
85
93
 
86
94
  this.agentConfig = agentConfig;
95
+ this.serveMode = options.serve === true;
96
+ this.servePort = options.servePort || 3001;
87
97
 
88
98
  // Build runner configuration with model resolution
89
99
  this.runnerConfig = buildRunnerConfig({
@@ -109,6 +119,7 @@ export class AgentDaemon {
109
119
  // Register with hub first (needed for assignment check)
110
120
  console.log("Registering with AgentMesh hub...");
111
121
  const existingState = getAgentState(this.agentName);
122
+ console.log(`Existing state: ${existingState ? `agentId=${existingState.agentId}` : "none"}`);
112
123
 
113
124
  const registration = await registerAgent({
114
125
  url: this.config.hubUrl,
@@ -127,39 +138,45 @@ export class AgentDaemon {
127
138
  // Check assignments and auto-setup workdir if needed (before creating tmux session)
128
139
  await this.checkAssignments();
129
140
 
130
- // Check if session already exists
131
- const sessionName = getSessionName(this.agentName);
132
- const sessionAlreadyExists = sessionExists(sessionName);
133
-
134
- // Create tmux session if it doesn't exist
135
- if (!sessionAlreadyExists) {
136
- console.log(`Creating tmux session: ${sessionName}`);
137
-
138
- // Include runner env vars (e.g., OPENCODE_MODEL) at session creation
139
- const created = createSession(
140
- this.agentName,
141
- this.agentConfig.command,
142
- this.agentConfig.workdir,
143
- this.runnerConfig.env, // Apply model env at process start
144
- );
145
-
146
- if (!created) {
147
- throw new Error("Failed to create tmux session");
148
- }
141
+ // Serve mode: start opencode serve instead of tmux
142
+ if (this.serveMode) {
143
+ await this.startServeMode();
149
144
  } else {
150
- console.log(`Reconnecting to existing session: ${sessionName}`);
151
- // Update environment for existing session
152
- updateSessionEnvironment(this.agentName, this.runnerConfig.env);
153
- }
145
+ // Check if session already exists
146
+ const sessionName = getSessionName(this.agentName);
147
+ const sessionAlreadyExists = sessionExists(sessionName);
148
+
149
+ // Create tmux session if it doesn't exist
150
+ if (!sessionAlreadyExists) {
151
+ console.log(`Creating tmux session: ${sessionName}`);
152
+
153
+ // Include runner env vars (e.g., OPENCODE_MODEL) at session creation
154
+ const created = createSession(
155
+ this.agentName,
156
+ this.agentConfig.command,
157
+ this.agentConfig.workdir,
158
+ this.runnerConfig.env, // Apply model env at process start
159
+ );
160
+
161
+ if (!created) {
162
+ throw new Error("Failed to create tmux session");
163
+ }
164
+ } else {
165
+ console.log(`Reconnecting to existing session: ${sessionName}`);
166
+ // Update environment for existing session
167
+ updateSessionEnvironment(this.agentName, this.runnerConfig.env);
168
+ }
154
169
 
155
- // Inject environment variables into tmux session
156
- console.log("Injecting environment variables...");
157
- updateSessionEnvironment(this.agentName, {
158
- AGENT_TOKEN: this.token,
159
- AGENTMESH_AGENT_ID: this.agentId,
160
- });
170
+ // Inject environment variables into tmux session
171
+ console.log("Injecting environment variables...");
172
+ updateSessionEnvironment(this.agentName, {
173
+ AGENT_TOKEN: this.token,
174
+ AGENTMESH_AGENT_ID: this.agentId,
175
+ });
176
+ }
161
177
 
162
178
  // Save state including runtime model info
179
+ const sessionName = this.serveMode ? `serve:${this.servePort}` : getSessionName(this.agentName);
163
180
  addAgentToState({
164
181
  name: this.agentName,
165
182
  agentId: this.agentId,
@@ -167,6 +184,8 @@ export class AgentDaemon {
167
184
  tmuxSession: sessionName,
168
185
  startedAt: new Date().toISOString(),
169
186
  token: this.token,
187
+ workdir: this.agentConfig.workdir,
188
+ assignedProject: this.assignedProject,
170
189
  runtimeModel: this.runnerConfig.model,
171
190
  runnerType: this.runnerConfig.type,
172
191
  });
@@ -208,6 +227,7 @@ export class AgentDaemon {
208
227
  url: `${wsUrl}/ws/v1`,
209
228
  token: newToken,
210
229
  onMessage: (event) => {
230
+ console.log(`[WS] Received event: ${event.type}`);
211
231
  handleWebSocketEvent(this.agentName, event);
212
232
  },
213
233
  onConnect: () => {
@@ -234,7 +254,11 @@ export class AgentDaemon {
234
254
  url: `${wsUrl}/ws/v1`,
235
255
  token: this.token,
236
256
  onMessage: (event) => {
237
- handleWebSocketEvent(this.agentName, event);
257
+ console.log(`[WS] Received event: ${event.type}`);
258
+ handleWebSocketEvent(this.agentName, event, {
259
+ hubUrl: this.config.hubUrl,
260
+ token: this.token ?? undefined,
261
+ });
238
262
  },
239
263
  onConnect: () => {
240
264
  console.log("WebSocket connected");
@@ -313,16 +337,78 @@ Nudge agent:
313
337
  this.ws = null;
314
338
  }
315
339
 
316
- // Destroy tmux session
317
- destroySession(this.agentName);
340
+ // Stop serve process or destroy tmux session
341
+ if (this.serveMode && this.serveProcess) {
342
+ console.log("Stopping opencode serve...");
343
+ this.serveProcess.kill("SIGTERM");
344
+ this.serveProcess = null;
345
+ } else {
346
+ destroySession(this.agentName);
347
+ }
318
348
 
319
- // Remove from state
320
- removeAgentFromState(this.agentName);
349
+ // Update state to mark as stopped but preserve agentId for next restart
350
+ updateAgentInState(this.agentName, {
351
+ pid: 0,
352
+ tmuxSession: "",
353
+ startedAt: "",
354
+ token: undefined,
355
+ });
321
356
 
322
357
  console.log("Agent stopped.");
323
358
  process.exit(0);
324
359
  }
325
360
 
361
+ /**
362
+ * Starts opencode serve mode (for Integration Service)
363
+ * Replaces tmux with a direct HTTP server
364
+ */
365
+ private async startServeMode(): Promise<void> {
366
+ console.log(`Starting opencode serve mode on port ${this.servePort}...`);
367
+
368
+ const workdir = this.agentConfig.workdir || process.cwd();
369
+
370
+ // Build environment for opencode serve
371
+ const env: Record<string, string> = {
372
+ ...process.env,
373
+ ...this.runnerConfig.env,
374
+ AGENT_TOKEN: this.token!,
375
+ AGENTMESH_AGENT_ID: this.agentId!,
376
+ } as Record<string, string>;
377
+
378
+ // Spawn opencode serve as a child process
379
+ this.serveProcess = spawn(
380
+ "opencode",
381
+ ["serve", "--port", String(this.servePort), "--hostname", "0.0.0.0"],
382
+ {
383
+ cwd: workdir,
384
+ env,
385
+ stdio: ["ignore", "inherit", "inherit"],
386
+ },
387
+ );
388
+
389
+ // Handle process exit
390
+ this.serveProcess.on("exit", (code, signal) => {
391
+ console.error(`opencode serve exited with code ${code}, signal ${signal}`);
392
+ if (this.isRunning) {
393
+ console.log("Restarting opencode serve in 5 seconds...");
394
+ setTimeout(() => {
395
+ if (this.isRunning) {
396
+ this.startServeMode().catch(console.error);
397
+ }
398
+ }, 5000);
399
+ }
400
+ });
401
+
402
+ this.serveProcess.on("error", (error) => {
403
+ console.error("Failed to start opencode serve:", error);
404
+ });
405
+
406
+ // Wait a moment for the server to start
407
+ await new Promise((resolve) => setTimeout(resolve, 2000));
408
+
409
+ console.log(`opencode serve started on http://0.0.0.0:${this.servePort}`);
410
+ }
411
+
326
412
  /**
327
413
  * Saves the current agent context to disk
328
414
  */
@@ -375,6 +461,8 @@ Nudge agent:
375
461
  console.log(` - ${assignment.project.name} (${assignment.role})${repoInfo}${workdirInfo}`);
376
462
  }
377
463
 
464
+ this.assignedProject = assignments[0]?.project.name;
465
+
378
466
  // If no CLI workdir specified, try to use project.workdir from HQ
379
467
  if (!this.agentConfig.workdir) {
380
468
  const assignmentWithWorkdir = assignments.find((a) => a.project.workdir);
@@ -1,3 +1,5 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { formatHandoffContextSummary, parseHandoffContext } from "../context/handoff.js";
2
4
  import type { AgentContext } from "../context/schema.js";
3
5
  import type { InboxItem } from "./registry.js";
@@ -65,6 +67,85 @@ You can now proceed with your work.`;
65
67
  sendKeys(agentName, message);
66
68
  }
67
69
 
70
+ export interface EventContext {
71
+ hubUrl?: string;
72
+ token?: string;
73
+ }
74
+
75
+ interface SlackFile {
76
+ name: string;
77
+ type: string;
78
+ url: string;
79
+ permalink: string;
80
+ base64?: string;
81
+ mediaType?: string;
82
+ }
83
+
84
+ export function injectSlackMessage(
85
+ agentName: string,
86
+ event: WebSocketEvent,
87
+ context?: EventContext,
88
+ ): void {
89
+ const user = (event.user as string) || "unknown";
90
+ const channel = (event.channel as string) || "unknown";
91
+ const text = (event.text as string) || "";
92
+ const files = (event.files as SlackFile[] | undefined) || [];
93
+
94
+ // Build message with file info
95
+ let message = `[Slack from ${user} in ${channel}] ${text}`;
96
+
97
+ if (files.length > 0) {
98
+ const savedImages: string[] = [];
99
+ const otherFiles: string[] = [];
100
+
101
+ // Save images with base64 data to disk
102
+ const attachmentsDir = join(process.env.HOME || "/tmp", ".agentmesh", "attachments");
103
+ if (!existsSync(attachmentsDir)) {
104
+ mkdirSync(attachmentsDir, { recursive: true });
105
+ }
106
+
107
+ for (const f of files) {
108
+ console.log(
109
+ `[Injector] File: ${f.name}, type: ${f.type}, mediaType: ${f.mediaType}, has base64: ${!!f.base64}`,
110
+ );
111
+ if (f.base64 && (f.mediaType?.startsWith("image/") || f.type?.startsWith("image/"))) {
112
+ // Save image to disk
113
+ const timestamp = Date.now();
114
+ const safeName = f.name.replace(/[^a-zA-Z0-9._-]/g, "_");
115
+ const filePath = join(attachmentsDir, `${timestamp}-${safeName}`);
116
+
117
+ try {
118
+ writeFileSync(filePath, Buffer.from(f.base64, "base64"));
119
+ savedImages.push(filePath);
120
+ console.log(`[Injector] Saved image to: ${filePath}`);
121
+ } catch (err) {
122
+ console.error(`[Injector] Failed to save image:`, err);
123
+ otherFiles.push(`${f.name} (failed to save)`);
124
+ }
125
+ } else {
126
+ // Just reference the URL for non-image files
127
+ otherFiles.push(`${f.name}: ${f.permalink}`);
128
+ }
129
+ }
130
+
131
+ // Add instruction to view images - OpenCode needs explicit instruction
132
+ if (savedImages.length > 0) {
133
+ message += `\n\nUser attached ${savedImages.length} image(s). Use the Read tool to view them:`;
134
+ for (const img of savedImages) {
135
+ message += `\n- ${img}`;
136
+ }
137
+ }
138
+
139
+ if (otherFiles.length > 0) {
140
+ message += ` [Other files: ${otherFiles.join(" | ")}]`;
141
+ }
142
+ }
143
+
144
+ console.log(`[Injector] Sending to tmux: ${message}`);
145
+ const result = sendKeys(agentName, message);
146
+ console.log(`[Injector] sendKeys result: ${result}`);
147
+ }
148
+
68
149
  export function injectInboxItems(agentName: string, items: InboxItem[]): void {
69
150
  if (items.length === 0) {
70
151
  sendKeys(agentName, "[AgentMesh] Your inbox is empty.");
@@ -85,7 +166,11 @@ export function injectInboxItems(agentName: string, items: InboxItem[]): void {
85
166
  sendKeys(agentName, message);
86
167
  }
87
168
 
88
- export function handleWebSocketEvent(agentName: string, event: WebSocketEvent): void {
169
+ export function handleWebSocketEvent(
170
+ agentName: string,
171
+ event: WebSocketEvent,
172
+ context?: EventContext,
173
+ ): void {
89
174
  switch (event.type) {
90
175
  case "handoff_received":
91
176
  case "handoff.received":
@@ -102,6 +187,18 @@ export function handleWebSocketEvent(agentName: string, event: WebSocketEvent):
102
187
  injectBlockerResolved(agentName, event);
103
188
  break;
104
189
 
190
+ case "slack.message":
191
+ console.log(`[Injector] Handling slack.message for ${agentName}`);
192
+ console.log(`[Injector] Event keys: ${Object.keys(event).join(", ")}`);
193
+ if (event.files) {
194
+ const files = event.files as Array<{ name: string; base64?: string }>;
195
+ console.log(
196
+ `[Injector] Files count: ${files.length}, has base64: ${files.some((f) => !!f.base64)}`,
197
+ );
198
+ }
199
+ injectSlackMessage(agentName, event, context);
200
+ break;
201
+
105
202
  default:
106
203
  // Unknown event type, ignore
107
204
  break;
package/src/core/tmux.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ChildProcess, execSync, spawn } from "node:child_process";
1
+ import { type ChildProcess, execFileSync, execSync, spawn } from "node:child_process";
2
2
 
3
3
  const SESSION_PREFIX = "agentmesh-";
4
4
 
@@ -56,7 +56,8 @@ export function createSession(
56
56
 
57
57
  const fullCommand = `${envPrefix}${command}`;
58
58
 
59
- const args = ["new-session", "-d", "-s", sessionName];
59
+ // Set reasonable terminal size for TUI applications
60
+ const args = ["new-session", "-d", "-s", sessionName, "-x", "200", "-y", "50"];
60
61
 
61
62
  if (workdir) {
62
63
  args.push("-c", workdir);
@@ -64,7 +65,7 @@ export function createSession(
64
65
 
65
66
  args.push(fullCommand);
66
67
 
67
- execSync(`tmux ${args.join(" ")}`);
68
+ execFileSync("tmux", args);
68
69
 
69
70
  // Also set session environment for any subsequent processes/refreshes
70
71
  if (env) {
@@ -82,7 +83,7 @@ export function setSessionEnvironment(sessionName: string, env: SessionEnv): boo
82
83
  try {
83
84
  for (const [key, value] of Object.entries(env)) {
84
85
  if (value !== undefined && value !== "") {
85
- execSync(`tmux set-environment -t "${sessionName}" ${key} "${value}"`);
86
+ execFileSync("tmux", ["set-environment", "-t", sessionName, key, value]);
86
87
  }
87
88
  }
88
89
  return true;
@@ -128,16 +129,13 @@ export function sendKeys(agentName: string, message: string): boolean {
128
129
  }
129
130
 
130
131
  try {
131
- // Escape special characters for tmux
132
- const escapedMessage = message
133
- .replace(/\\/g, "\\\\")
134
- .replace(/"/g, '\\"')
135
- .replace(/\$/g, "\\$")
136
- .replace(/`/g, "\\`");
137
-
138
- // Send the message, then press Enter separately to submit to the AI
139
- execSync(`tmux send-keys -t "${sessionName}" "${escapedMessage}"`);
140
- execSync(`tmux send-keys -t "${sessionName}" Enter`);
132
+ // Replace newlines with " | " to keep message on single line (newlines would act as Enter)
133
+ const cleanMessage = message.replace(/\n/g, " | ");
134
+
135
+ // Use execFileSync with array args to avoid shell escaping issues
136
+ // The -l flag sends keys literally
137
+ execFileSync("tmux", ["send-keys", "-t", sessionName, "-l", cleanMessage]);
138
+ execFileSync("tmux", ["send-keys", "-t", sessionName, "Enter"]);
141
139
  return true;
142
140
  } catch (error) {
143
141
  console.error(`Failed to send keys: ${error}`);