@agentmeshhq/agent 0.1.11 → 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 (47) 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/local.d.ts +9 -0
  13. package/dist/cli/local.js +139 -0
  14. package/dist/cli/local.js.map +1 -0
  15. package/dist/cli/migrate.d.ts +8 -0
  16. package/dist/cli/migrate.js +167 -0
  17. package/dist/cli/migrate.js.map +1 -0
  18. package/dist/cli/slack.d.ts +3 -0
  19. package/dist/cli/slack.js +57 -0
  20. package/dist/cli/slack.js.map +1 -0
  21. package/dist/cli/start.d.ts +4 -0
  22. package/dist/cli/start.js +5 -0
  23. package/dist/cli/start.js.map +1 -1
  24. package/dist/cli/test.d.ts +8 -0
  25. package/dist/cli/test.js +110 -0
  26. package/dist/cli/test.js.map +1 -0
  27. package/dist/core/daemon.d.ts +12 -0
  28. package/dist/core/daemon.js +96 -27
  29. package/dist/core/daemon.js.map +1 -1
  30. package/dist/core/injector.d.ts +6 -1
  31. package/dist/core/injector.js +64 -1
  32. package/dist/core/injector.js.map +1 -1
  33. package/dist/core/tmux.js +8 -10
  34. package/dist/core/tmux.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/__tests__/injector.test.ts +29 -0
  37. package/src/cli/build.ts +137 -0
  38. package/src/cli/deploy.ts +153 -0
  39. package/src/cli/index.ts +156 -0
  40. package/src/cli/local.ts +174 -0
  41. package/src/cli/migrate.ts +210 -0
  42. package/src/cli/slack.ts +69 -0
  43. package/src/cli/start.ts +8 -0
  44. package/src/cli/test.ts +141 -0
  45. package/src/core/daemon.ts +118 -35
  46. package/src/core/injector.ts +98 -1
  47. package/src/core/tmux.ts +9 -11
@@ -0,0 +1,210 @@
1
+ import { spawnSync, execSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import pc from "picocolors";
4
+
5
+ function findProjectRoot(): string {
6
+ let dir = process.cwd();
7
+ for (let i = 0; i < 10; i++) {
8
+ const packageJson = path.join(dir, "package.json");
9
+ const pnpmWorkspace = path.join(dir, "pnpm-workspace.yaml");
10
+ try {
11
+ execSync(`test -f "${packageJson}" && test -f "${pnpmWorkspace}"`, { stdio: "ignore" });
12
+ return dir;
13
+ } catch {
14
+ dir = path.dirname(dir);
15
+ }
16
+ }
17
+ throw new Error("Could not find AgentMesh project root. Make sure you're in the agentmesh repository.");
18
+ }
19
+
20
+ function isLocalStackRunning(): boolean {
21
+ try {
22
+ const result = execSync(
23
+ 'docker inspect --format="{{.State.Running}}" agentmesh_postgres 2>/dev/null',
24
+ { encoding: "utf-8" }
25
+ ).trim();
26
+ return result === "true";
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ function getLocalDbUrl(): string {
33
+ // Default local stack connection
34
+ return "postgresql://postgres:postgres@localhost:5432/agentmesh";
35
+ }
36
+
37
+ export interface MigrateOptions {
38
+ dev?: boolean;
39
+ staging?: boolean;
40
+ generate?: boolean;
41
+ status?: boolean;
42
+ rollback?: boolean;
43
+ }
44
+
45
+ export async function migrate(options: MigrateOptions = {}): Promise<void> {
46
+ const projectRoot = findProjectRoot();
47
+
48
+ if (options.generate) {
49
+ await generateMigration(projectRoot);
50
+ return;
51
+ }
52
+
53
+ if (options.status) {
54
+ await migrationStatus(projectRoot, options);
55
+ return;
56
+ }
57
+
58
+ if (options.rollback) {
59
+ console.error(pc.red("Rollback is not supported. Use a new migration to revert changes."));
60
+ process.exit(1);
61
+ }
62
+
63
+ await runMigrations(projectRoot, options);
64
+ }
65
+
66
+ async function generateMigration(projectRoot: string): Promise<void> {
67
+ console.log(pc.cyan("Generating migration from schema changes..."));
68
+ console.log();
69
+
70
+ const result = spawnSync("pnpm", ["--filter", "@agentmesh/hub", "db:generate"], {
71
+ cwd: projectRoot,
72
+ stdio: "inherit",
73
+ });
74
+
75
+ if (result.status !== 0) {
76
+ console.error(pc.red("Migration generation failed"));
77
+ process.exit(1);
78
+ }
79
+
80
+ console.log();
81
+ console.log(pc.green("Migration generated successfully!"));
82
+ console.log(pc.dim("Review the migration in packages/hub/drizzle/"));
83
+ }
84
+
85
+ async function migrationStatus(projectRoot: string, options: MigrateOptions): Promise<void> {
86
+ const env = getEnvironmentConfig(options);
87
+
88
+ console.log(pc.cyan(`Checking migration status for ${env.name}...`));
89
+ console.log();
90
+
91
+ // Check if we can connect
92
+ if (!env.canConnect) {
93
+ console.error(pc.red(`Cannot connect to ${env.name}: ${env.reason}`));
94
+ process.exit(1);
95
+ }
96
+
97
+ const result = spawnSync("pnpm", ["--filter", "@agentmesh/hub", "db:status"], {
98
+ cwd: projectRoot,
99
+ stdio: "inherit",
100
+ env: {
101
+ ...process.env,
102
+ DATABASE_URL: env.databaseUrl,
103
+ },
104
+ });
105
+
106
+ if (result.status !== 0) {
107
+ console.error(pc.red("Failed to check migration status"));
108
+ process.exit(1);
109
+ }
110
+ }
111
+
112
+ interface EnvironmentConfig {
113
+ name: string;
114
+ databaseUrl: string;
115
+ canConnect: boolean;
116
+ reason?: string;
117
+ }
118
+
119
+ function getEnvironmentConfig(options: MigrateOptions): EnvironmentConfig {
120
+ if (options.dev) {
121
+ // Dev environment uses cloud database via environment variable
122
+ const devUrl = process.env.AGENTMESH_DEV_DATABASE_URL;
123
+ if (!devUrl) {
124
+ return {
125
+ name: "dev (agentmeshhq.dev)",
126
+ databaseUrl: "",
127
+ canConnect: false,
128
+ reason: "AGENTMESH_DEV_DATABASE_URL environment variable not set",
129
+ };
130
+ }
131
+ return {
132
+ name: "dev (agentmeshhq.dev)",
133
+ databaseUrl: devUrl,
134
+ canConnect: true,
135
+ };
136
+ }
137
+
138
+ if (options.staging) {
139
+ const stagingUrl = process.env.AGENTMESH_STAGING_DATABASE_URL;
140
+ if (!stagingUrl) {
141
+ return {
142
+ name: "staging",
143
+ databaseUrl: "",
144
+ canConnect: false,
145
+ reason: "AGENTMESH_STAGING_DATABASE_URL environment variable not set",
146
+ };
147
+ }
148
+ return {
149
+ name: "staging",
150
+ databaseUrl: stagingUrl,
151
+ canConnect: true,
152
+ };
153
+ }
154
+
155
+ // Default: local stack
156
+ const localRunning = isLocalStackRunning();
157
+ if (!localRunning) {
158
+ return {
159
+ name: "local",
160
+ databaseUrl: getLocalDbUrl(),
161
+ canConnect: false,
162
+ reason: "Local stack is not running. Start it with: agentmesh local up",
163
+ };
164
+ }
165
+
166
+ return {
167
+ name: "local",
168
+ databaseUrl: getLocalDbUrl(),
169
+ canConnect: true,
170
+ };
171
+ }
172
+
173
+ async function runMigrations(projectRoot: string, options: MigrateOptions): Promise<void> {
174
+ const env = getEnvironmentConfig(options);
175
+
176
+ console.log(pc.cyan(`Running migrations against ${env.name}...`));
177
+ console.log();
178
+
179
+ if (!env.canConnect) {
180
+ console.error(pc.red(`Cannot connect to ${env.name}: ${env.reason}`));
181
+ process.exit(1);
182
+ }
183
+
184
+ // Safety confirmation for non-local environments
185
+ if (options.dev || options.staging) {
186
+ console.log(pc.yellow(`WARNING: You are about to run migrations against ${env.name}`));
187
+ console.log(pc.yellow("This will modify the database schema."));
188
+ console.log();
189
+ console.log(pc.dim("Press Ctrl+C to cancel, or wait 5 seconds to continue..."));
190
+ await new Promise((resolve) => setTimeout(resolve, 5000));
191
+ console.log();
192
+ }
193
+
194
+ const result = spawnSync("pnpm", ["--filter", "@agentmesh/hub", "db:migrate"], {
195
+ cwd: projectRoot,
196
+ stdio: "inherit",
197
+ env: {
198
+ ...process.env,
199
+ DATABASE_URL: env.databaseUrl,
200
+ },
201
+ });
202
+
203
+ if (result.status !== 0) {
204
+ console.error(pc.red("Migration failed"));
205
+ process.exit(1);
206
+ }
207
+
208
+ console.log();
209
+ console.log(pc.green(`Migrations applied successfully to ${env.name}!`));
210
+ }
@@ -0,0 +1,69 @@
1
+ import { loadConfig, getAgentState } from "../config/loader.js";
2
+ import pc from "picocolors";
3
+
4
+ export async function slack(
5
+ action: string,
6
+ channel: string | undefined,
7
+ message: string | undefined,
8
+ options: { name?: string }
9
+ ): Promise<void> {
10
+ const config = loadConfig();
11
+
12
+ if (!config) {
13
+ console.log(pc.red("No config found. Run 'agentmesh init' first."));
14
+ process.exit(1);
15
+ }
16
+
17
+ if (action === "respond" || action === "reply") {
18
+ if (!channel) {
19
+ console.log(pc.red("Channel is required. Usage: agentmesh slack respond <channel> <message>"));
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!message) {
24
+ console.log(pc.red("Message is required. Usage: agentmesh slack respond <channel> <message>"));
25
+ process.exit(1);
26
+ }
27
+
28
+ // Get token from agent state or config
29
+ const agentName = options.name || "concierge";
30
+ const agentState = getAgentState(agentName);
31
+ const token = agentState?.token;
32
+
33
+ if (!token) {
34
+ console.log(pc.red(`No token found for agent "${agentName}". Is the agent running?`));
35
+ process.exit(1);
36
+ }
37
+
38
+ try {
39
+ const response = await fetch(
40
+ `${config.hubUrl}/api/v1/integrations/slack/respond`,
41
+ {
42
+ method: "POST",
43
+ headers: {
44
+ Authorization: `Bearer ${token}`,
45
+ "Content-Type": "application/json",
46
+ },
47
+ body: JSON.stringify({ channel, text: message }),
48
+ }
49
+ );
50
+
51
+ if (response.ok) {
52
+ console.log(pc.green(`Message sent to Slack channel ${channel}`));
53
+ } else {
54
+ const error = await response.text();
55
+ console.log(pc.red(`Failed to send: ${error}`));
56
+ process.exit(1);
57
+ }
58
+ } catch (error) {
59
+ console.log(pc.red(`Failed to send: ${(error as Error).message}`));
60
+ process.exit(1);
61
+ }
62
+ } else {
63
+ console.log(pc.yellow("Usage:"));
64
+ console.log(" agentmesh slack respond <channel> <message> - Send a message to Slack");
65
+ console.log("");
66
+ console.log(pc.dim("Examples:"));
67
+ console.log(' agentmesh slack respond C0123456 "Hello from AgentMesh!"');
68
+ }
69
+ }
package/src/cli/start.ts CHANGED
@@ -14,6 +14,10 @@ export interface StartOptions {
14
14
  foreground?: boolean;
15
15
  noContext?: boolean;
16
16
  autoSetup?: boolean;
17
+ /** Run opencode serve instead of tmux TUI (for Integration Service) */
18
+ serve?: boolean;
19
+ /** Port for opencode serve (default: 3001) */
20
+ servePort?: number;
17
21
  }
18
22
 
19
23
  export async function start(options: StartOptions): Promise<void> {
@@ -70,6 +74,10 @@ export async function start(options: StartOptions): Promise<void> {
70
74
  if (options.model) args.push("--model", options.model);
71
75
  if (options.noContext) args.push("--no-context");
72
76
  if (options.autoSetup) args.push("--auto-setup");
77
+ if (options.serve) {
78
+ args.push("--serve");
79
+ if (options.servePort) args.push("--serve-port", String(options.servePort));
80
+ }
73
81
 
74
82
  // Spawn detached background process
75
83
  const child = spawn("node", [cliPath, ...args], {
@@ -0,0 +1,141 @@
1
+ import { spawnSync, execSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import pc from "picocolors";
4
+
5
+ function findProjectRoot(): string {
6
+ let dir = process.cwd();
7
+ for (let i = 0; i < 10; i++) {
8
+ const packageJson = path.join(dir, "package.json");
9
+ const pnpmWorkspace = path.join(dir, "pnpm-workspace.yaml");
10
+ try {
11
+ execSync(`test -f "${packageJson}" && test -f "${pnpmWorkspace}"`, { stdio: "ignore" });
12
+ return dir;
13
+ } catch {
14
+ dir = path.dirname(dir);
15
+ }
16
+ }
17
+ throw new Error("Could not find AgentMesh project root. Make sure you're in the agentmesh repository.");
18
+ }
19
+
20
+ function isLocalStackRunning(): boolean {
21
+ try {
22
+ const result = execSync(
23
+ 'docker inspect --format="{{.State.Running}}" agentmesh_postgres 2>/dev/null',
24
+ { encoding: "utf-8" }
25
+ ).trim();
26
+ return result === "true";
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ export interface TestOptions {
33
+ package?: string;
34
+ e2e?: boolean;
35
+ watch?: boolean;
36
+ coverage?: boolean;
37
+ updateSnapshots?: boolean;
38
+ }
39
+
40
+ export async function test(options: TestOptions = {}): Promise<void> {
41
+ const projectRoot = findProjectRoot();
42
+
43
+ if (options.e2e) {
44
+ await runE2ETests(projectRoot, options);
45
+ } else {
46
+ await runUnitTests(projectRoot, options);
47
+ }
48
+ }
49
+
50
+ async function runUnitTests(projectRoot: string, options: TestOptions): Promise<void> {
51
+ console.log(pc.cyan("Running unit tests..."));
52
+
53
+ // Check if local stack is running for integration tests
54
+ const localStackUp = isLocalStackRunning();
55
+ if (localStackUp) {
56
+ console.log(pc.dim("Local stack detected - integration tests will use local services"));
57
+ }
58
+
59
+ const args = ["pnpm"];
60
+
61
+ if (options.package) {
62
+ args.push("--filter", options.package);
63
+ }
64
+
65
+ args.push("test");
66
+
67
+ if (options.watch) {
68
+ args.push("--", "--watch");
69
+ }
70
+
71
+ if (options.coverage) {
72
+ args.push("--", "--coverage");
73
+ }
74
+
75
+ if (options.updateSnapshots) {
76
+ args.push("--", "-u");
77
+ }
78
+
79
+ console.log();
80
+
81
+ const result = spawnSync(args[0], args.slice(1), {
82
+ cwd: projectRoot,
83
+ stdio: "inherit",
84
+ env: {
85
+ ...process.env,
86
+ // If local stack is running, use it for tests
87
+ ...(localStackUp && {
88
+ DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/agentmesh",
89
+ REDIS_URL: "redis://localhost:6380/0",
90
+ }),
91
+ },
92
+ });
93
+
94
+ if (result.status !== 0) {
95
+ console.error(pc.red("Tests failed"));
96
+ process.exit(1);
97
+ }
98
+
99
+ console.log();
100
+ console.log(pc.green("All tests passed!"));
101
+ }
102
+
103
+ async function runE2ETests(projectRoot: string, options: TestOptions): Promise<void> {
104
+ console.log(pc.cyan("Running E2E tests..."));
105
+
106
+ // E2E tests require the local stack to be running
107
+ const localStackUp = isLocalStackRunning();
108
+ if (!localStackUp) {
109
+ console.log(pc.yellow("Warning: Local stack is not running."));
110
+ console.log(pc.dim("Start it with: agentmesh local up"));
111
+ console.log();
112
+ }
113
+
114
+ const args = ["pnpm", "--filter", "@agentmesh/admin", "test:e2e"];
115
+
116
+ if (options.updateSnapshots) {
117
+ args.push("--", "--update-snapshots");
118
+ }
119
+
120
+ console.log();
121
+
122
+ const result = spawnSync(args[0], args.slice(1), {
123
+ cwd: projectRoot,
124
+ stdio: "inherit",
125
+ env: {
126
+ ...process.env,
127
+ // Point to local stack if running
128
+ ...(localStackUp && {
129
+ PLAYWRIGHT_BASE_URL: "http://localhost:3000",
130
+ }),
131
+ },
132
+ });
133
+
134
+ if (result.status !== 0) {
135
+ console.error(pc.red("E2E tests failed"));
136
+ process.exit(1);
137
+ }
138
+
139
+ console.log();
140
+ console.log(pc.green("E2E tests passed!"));
141
+ }
@@ -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 {
@@ -55,6 +59,9 @@ export class AgentDaemon {
55
59
  private assignedProject: string | undefined;
56
60
  private shouldRestoreContext: boolean;
57
61
  private autoSetup: boolean;
62
+ private serveMode: boolean;
63
+ private servePort: number;
64
+ private serveProcess: ChildProcess | null = null;
58
65
 
59
66
  constructor(options: DaemonOptions) {
60
67
  const config = loadConfig();
@@ -85,6 +92,8 @@ export class AgentDaemon {
85
92
  if (options.model) agentConfig.model = options.model;
86
93
 
87
94
  this.agentConfig = agentConfig;
95
+ this.serveMode = options.serve === true;
96
+ this.servePort = options.servePort || 3001;
88
97
 
89
98
  // Build runner configuration with model resolution
90
99
  this.runnerConfig = buildRunnerConfig({
@@ -110,6 +119,7 @@ export class AgentDaemon {
110
119
  // Register with hub first (needed for assignment check)
111
120
  console.log("Registering with AgentMesh hub...");
112
121
  const existingState = getAgentState(this.agentName);
122
+ console.log(`Existing state: ${existingState ? `agentId=${existingState.agentId}` : "none"}`);
113
123
 
114
124
  const registration = await registerAgent({
115
125
  url: this.config.hubUrl,
@@ -128,39 +138,45 @@ export class AgentDaemon {
128
138
  // Check assignments and auto-setup workdir if needed (before creating tmux session)
129
139
  await this.checkAssignments();
130
140
 
131
- // Check if session already exists
132
- const sessionName = getSessionName(this.agentName);
133
- const sessionAlreadyExists = sessionExists(sessionName);
134
-
135
- // Create tmux session if it doesn't exist
136
- if (!sessionAlreadyExists) {
137
- console.log(`Creating tmux session: ${sessionName}`);
138
-
139
- // Include runner env vars (e.g., OPENCODE_MODEL) at session creation
140
- const created = createSession(
141
- this.agentName,
142
- this.agentConfig.command,
143
- this.agentConfig.workdir,
144
- this.runnerConfig.env, // Apply model env at process start
145
- );
146
-
147
- if (!created) {
148
- throw new Error("Failed to create tmux session");
149
- }
141
+ // Serve mode: start opencode serve instead of tmux
142
+ if (this.serveMode) {
143
+ await this.startServeMode();
150
144
  } else {
151
- console.log(`Reconnecting to existing session: ${sessionName}`);
152
- // Update environment for existing session
153
- updateSessionEnvironment(this.agentName, this.runnerConfig.env);
154
- }
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
+ }
155
169
 
156
- // Inject environment variables into tmux session
157
- console.log("Injecting environment variables...");
158
- updateSessionEnvironment(this.agentName, {
159
- AGENT_TOKEN: this.token,
160
- AGENTMESH_AGENT_ID: this.agentId,
161
- });
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
+ }
162
177
 
163
178
  // Save state including runtime model info
179
+ const sessionName = this.serveMode ? `serve:${this.servePort}` : getSessionName(this.agentName);
164
180
  addAgentToState({
165
181
  name: this.agentName,
166
182
  agentId: this.agentId,
@@ -211,6 +227,7 @@ export class AgentDaemon {
211
227
  url: `${wsUrl}/ws/v1`,
212
228
  token: newToken,
213
229
  onMessage: (event) => {
230
+ console.log(`[WS] Received event: ${event.type}`);
214
231
  handleWebSocketEvent(this.agentName, event);
215
232
  },
216
233
  onConnect: () => {
@@ -237,7 +254,11 @@ export class AgentDaemon {
237
254
  url: `${wsUrl}/ws/v1`,
238
255
  token: this.token,
239
256
  onMessage: (event) => {
240
- 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
+ });
241
262
  },
242
263
  onConnect: () => {
243
264
  console.log("WebSocket connected");
@@ -316,16 +337,78 @@ Nudge agent:
316
337
  this.ws = null;
317
338
  }
318
339
 
319
- // Destroy tmux session
320
- 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
+ }
321
348
 
322
- // Remove from state
323
- 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
+ });
324
356
 
325
357
  console.log("Agent stopped.");
326
358
  process.exit(0);
327
359
  }
328
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
+
329
412
  /**
330
413
  * Saves the current agent context to disk
331
414
  */