@agentmeshhq/agent 0.1.12 → 0.1.14

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 (117) hide show
  1. package/dist/__tests__/loader.test.js +44 -1
  2. package/dist/__tests__/loader.test.js.map +1 -1
  3. package/dist/__tests__/runner.test.js.map +1 -1
  4. package/dist/__tests__/sandbox.test.d.ts +1 -0
  5. package/dist/__tests__/sandbox.test.js +362 -0
  6. package/dist/__tests__/sandbox.test.js.map +1 -0
  7. package/dist/__tests__/watchdog.test.d.ts +1 -0
  8. package/dist/__tests__/watchdog.test.js +290 -0
  9. package/dist/__tests__/watchdog.test.js.map +1 -0
  10. package/dist/cli/attach.js +20 -1
  11. package/dist/cli/attach.js.map +1 -1
  12. package/dist/cli/build.js +8 -2
  13. package/dist/cli/build.js.map +1 -1
  14. package/dist/cli/context.js.map +1 -1
  15. package/dist/cli/deploy.js +1 -1
  16. package/dist/cli/deploy.js.map +1 -1
  17. package/dist/cli/inbox.d.ts +5 -0
  18. package/dist/cli/inbox.js +123 -0
  19. package/dist/cli/inbox.js.map +1 -0
  20. package/dist/cli/index.js +5 -1
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/init.js +1 -1
  23. package/dist/cli/init.js.map +1 -1
  24. package/dist/cli/issue.d.ts +42 -0
  25. package/dist/cli/issue.js +297 -0
  26. package/dist/cli/issue.js.map +1 -0
  27. package/dist/cli/list.js +3 -3
  28. package/dist/cli/list.js.map +1 -1
  29. package/dist/cli/local.js +5 -3
  30. package/dist/cli/local.js.map +1 -1
  31. package/dist/cli/migrate.js +1 -1
  32. package/dist/cli/migrate.js.map +1 -1
  33. package/dist/cli/nudge.js +16 -3
  34. package/dist/cli/nudge.js.map +1 -1
  35. package/dist/cli/ready.d.ts +5 -0
  36. package/dist/cli/ready.js +131 -0
  37. package/dist/cli/ready.js.map +1 -0
  38. package/dist/cli/restart.js.map +1 -1
  39. package/dist/cli/slack.js +1 -1
  40. package/dist/cli/slack.js.map +1 -1
  41. package/dist/cli/start.d.ts +8 -0
  42. package/dist/cli/start.js +9 -0
  43. package/dist/cli/start.js.map +1 -1
  44. package/dist/cli/stop.js +13 -5
  45. package/dist/cli/stop.js.map +1 -1
  46. package/dist/cli/sync.d.ts +8 -0
  47. package/dist/cli/sync.js +154 -0
  48. package/dist/cli/sync.js.map +1 -0
  49. package/dist/cli/test.js +1 -1
  50. package/dist/cli/test.js.map +1 -1
  51. package/dist/cli/token.js +2 -2
  52. package/dist/cli/token.js.map +1 -1
  53. package/dist/config/loader.d.ts +5 -1
  54. package/dist/config/loader.js +27 -2
  55. package/dist/config/loader.js.map +1 -1
  56. package/dist/config/schema.d.ts +13 -0
  57. package/dist/core/daemon.d.ts +50 -0
  58. package/dist/core/daemon.js +445 -11
  59. package/dist/core/daemon.js.map +1 -1
  60. package/dist/core/injector.d.ts +2 -2
  61. package/dist/core/injector.js +23 -4
  62. package/dist/core/injector.js.map +1 -1
  63. package/dist/core/issue-cache.d.ts +44 -0
  64. package/dist/core/issue-cache.js +75 -0
  65. package/dist/core/issue-cache.js.map +1 -0
  66. package/dist/core/registry.d.ts +5 -0
  67. package/dist/core/registry.js +8 -1
  68. package/dist/core/registry.js.map +1 -1
  69. package/dist/core/runner.d.ts +1 -1
  70. package/dist/core/runner.js +23 -1
  71. package/dist/core/runner.js.map +1 -1
  72. package/dist/core/sandbox.d.ts +138 -0
  73. package/dist/core/sandbox.js +409 -0
  74. package/dist/core/sandbox.js.map +1 -0
  75. package/dist/core/tmux.d.ts +8 -0
  76. package/dist/core/tmux.js +28 -1
  77. package/dist/core/tmux.js.map +1 -1
  78. package/dist/core/watchdog.d.ts +41 -0
  79. package/dist/core/watchdog.js +198 -0
  80. package/dist/core/watchdog.js.map +1 -0
  81. package/dist/core/websocket.js +1 -1
  82. package/dist/core/websocket.js.map +1 -1
  83. package/dist/index.d.ts +5 -5
  84. package/dist/index.js +5 -5
  85. package/dist/index.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/__tests__/loader.test.ts +52 -4
  88. package/src/__tests__/runner.test.ts +1 -2
  89. package/src/__tests__/sandbox.test.ts +435 -0
  90. package/src/__tests__/watchdog.test.ts +368 -0
  91. package/src/cli/attach.ts +22 -1
  92. package/src/cli/build.ts +12 -4
  93. package/src/cli/context.ts +0 -1
  94. package/src/cli/deploy.ts +7 -5
  95. package/src/cli/index.ts +8 -1
  96. package/src/cli/init.ts +7 -19
  97. package/src/cli/list.ts +6 -10
  98. package/src/cli/local.ts +21 -12
  99. package/src/cli/migrate.ts +6 -4
  100. package/src/cli/nudge.ts +29 -14
  101. package/src/cli/restart.ts +1 -1
  102. package/src/cli/slack.ts +16 -15
  103. package/src/cli/start.ts +14 -0
  104. package/src/cli/stop.ts +14 -5
  105. package/src/cli/test.ts +5 -3
  106. package/src/cli/token.ts +4 -4
  107. package/src/config/loader.ts +29 -2
  108. package/src/config/schema.ts +14 -0
  109. package/src/core/daemon.ts +540 -17
  110. package/src/core/injector.ts +27 -4
  111. package/src/core/registry.ts +14 -1
  112. package/src/core/runner.ts +26 -1
  113. package/src/core/sandbox.ts +550 -0
  114. package/src/core/tmux.ts +35 -2
  115. package/src/core/watchdog.ts +238 -0
  116. package/src/core/websocket.ts +2 -2
  117. package/src/index.ts +6 -5
@@ -0,0 +1,368 @@
1
+ import { execSync, spawnSync } from "node:child_process";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import * as tmux from "../core/tmux.js";
4
+ import {
5
+ checkAgentProgress,
6
+ cleanupOrphanContainers,
7
+ detectPermissionPrompt,
8
+ findOrphanContainers,
9
+ getLastActivityTime,
10
+ isProcessRunning,
11
+ sendNudge,
12
+ type WatchdogResult,
13
+ } from "../core/watchdog.js";
14
+
15
+ // Mock child_process
16
+ vi.mock("node:child_process", () => ({
17
+ execSync: vi.fn(),
18
+ spawnSync: vi.fn(),
19
+ }));
20
+
21
+ // Mock tmux module
22
+ vi.mock("../core/tmux.js", () => ({
23
+ captureSessionOutput: vi.fn(),
24
+ }));
25
+
26
+ describe("Watchdog Module", () => {
27
+ beforeEach(() => {
28
+ vi.resetAllMocks();
29
+ });
30
+
31
+ describe("detectPermissionPrompt", () => {
32
+ it("should return null when no permission prompt detected", () => {
33
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue(
34
+ "Normal output without any prompts\nJust some code being written",
35
+ );
36
+
37
+ const result = detectPermissionPrompt("test-agent");
38
+ expect(result).toBeNull();
39
+ });
40
+
41
+ it("should detect 'Permission required' prompt", () => {
42
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue(
43
+ "Some output\nPermission required\nAllow once | Allow always | Reject",
44
+ );
45
+
46
+ const result = detectPermissionPrompt("test-agent");
47
+ expect(result).toBe("Permission prompt detected");
48
+ });
49
+
50
+ it("should detect external directory access prompt", () => {
51
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue(
52
+ "Access external directory /tmp/some-path\nAllow once | Allow always | Reject",
53
+ );
54
+
55
+ const result = detectPermissionPrompt("test-agent");
56
+ expect(result).toBe("External directory: /tmp/some-path");
57
+ });
58
+
59
+ it("should detect triangle permission prompt", () => {
60
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue(
61
+ "Some output\n△ Permission required\nWaiting for approval",
62
+ );
63
+
64
+ const result = detectPermissionPrompt("test-agent");
65
+ expect(result).toBe("Permission prompt detected");
66
+ });
67
+
68
+ it("should return null when capture fails", () => {
69
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue(null);
70
+
71
+ const result = detectPermissionPrompt("test-agent");
72
+ expect(result).toBeNull();
73
+ });
74
+
75
+ it("should return null when capture throws", () => {
76
+ vi.mocked(tmux.captureSessionOutput).mockImplementation(() => {
77
+ throw new Error("Session not found");
78
+ });
79
+
80
+ const result = detectPermissionPrompt("test-agent");
81
+ expect(result).toBeNull();
82
+ });
83
+ });
84
+
85
+ describe("getLastActivityTime", () => {
86
+ it("should parse timestamp from local log file", () => {
87
+ vi.mocked(spawnSync).mockReturnValue({
88
+ status: 0,
89
+ stdout: "INFO 2026-02-26T00:14:42 +0ms service=opencode message=test",
90
+ stderr: "",
91
+ pid: 123,
92
+ signal: null,
93
+ output: [],
94
+ });
95
+
96
+ const result = getLastActivityTime("test-agent");
97
+ expect(result).toBeInstanceOf(Date);
98
+ expect(result?.toISOString()).toContain("2026-02-26");
99
+ });
100
+
101
+ it("should parse timestamp from container log file", () => {
102
+ vi.mocked(spawnSync).mockReturnValue({
103
+ status: 0,
104
+ stdout: "INFO 2026-02-26T01:30:00 +0ms service=opencode",
105
+ stderr: "",
106
+ pid: 123,
107
+ signal: null,
108
+ output: [],
109
+ });
110
+
111
+ const result = getLastActivityTime("test-agent", "container-123");
112
+ expect(result).toBeInstanceOf(Date);
113
+ });
114
+
115
+ it("should return null when command fails", () => {
116
+ vi.mocked(spawnSync).mockReturnValue({
117
+ status: 1,
118
+ stdout: "",
119
+ stderr: "No such file",
120
+ pid: 123,
121
+ signal: null,
122
+ output: [],
123
+ });
124
+
125
+ const result = getLastActivityTime("test-agent");
126
+ expect(result).toBeNull();
127
+ });
128
+
129
+ it("should return null when no timestamp found", () => {
130
+ vi.mocked(spawnSync).mockReturnValue({
131
+ status: 0,
132
+ stdout: "Some output without timestamp",
133
+ stderr: "",
134
+ pid: 123,
135
+ signal: null,
136
+ output: [],
137
+ });
138
+
139
+ const result = getLastActivityTime("test-agent");
140
+ expect(result).toBeNull();
141
+ });
142
+
143
+ it("should return null on empty output", () => {
144
+ vi.mocked(spawnSync).mockReturnValue({
145
+ status: 0,
146
+ stdout: "",
147
+ stderr: "",
148
+ pid: 123,
149
+ signal: null,
150
+ output: [],
151
+ });
152
+
153
+ const result = getLastActivityTime("test-agent");
154
+ expect(result).toBeNull();
155
+ });
156
+ });
157
+
158
+ describe("checkAgentProgress", () => {
159
+ it("should return permission_blocked when permission prompt detected", () => {
160
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue("Permission required\nAllow once");
161
+
162
+ const result = checkAgentProgress("test-agent");
163
+ expect(result.status).toBe("permission_blocked");
164
+ expect(result.blockedOn).toBe("Permission prompt detected");
165
+ });
166
+
167
+ it("should return active when recent activity", () => {
168
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue("Normal output");
169
+
170
+ // Mock recent timestamp (1 minute ago)
171
+ const recentTime = new Date(Date.now() - 60 * 1000);
172
+ vi.mocked(spawnSync).mockReturnValue({
173
+ status: 0,
174
+ stdout: `INFO ${recentTime.toISOString().slice(0, 19)} +0ms service=opencode`,
175
+ stderr: "",
176
+ pid: 123,
177
+ signal: null,
178
+ output: [],
179
+ });
180
+
181
+ const result = checkAgentProgress("test-agent");
182
+ expect(result.status).toBe("active");
183
+ });
184
+
185
+ it("should return idle when no activity for 3 minutes", () => {
186
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue("Normal output");
187
+
188
+ // Mock timestamp 3 minutes ago
189
+ const idleTime = new Date(Date.now() - 3 * 60 * 1000);
190
+ vi.mocked(spawnSync).mockReturnValue({
191
+ status: 0,
192
+ stdout: `INFO ${idleTime.toISOString().slice(0, 19)} +0ms service=opencode`,
193
+ stderr: "",
194
+ pid: 123,
195
+ signal: null,
196
+ output: [],
197
+ });
198
+
199
+ const result = checkAgentProgress("test-agent");
200
+ expect(result.status).toBe("idle");
201
+ });
202
+
203
+ it("should return stuck when no activity for 6 minutes", () => {
204
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue("Normal output");
205
+
206
+ // Mock timestamp 6 minutes ago
207
+ const stuckTime = new Date(Date.now() - 6 * 60 * 1000);
208
+ vi.mocked(spawnSync).mockReturnValue({
209
+ status: 0,
210
+ stdout: `INFO ${stuckTime.toISOString().slice(0, 19)} +0ms service=opencode`,
211
+ stderr: "",
212
+ pid: 123,
213
+ signal: null,
214
+ output: [],
215
+ });
216
+
217
+ const result = checkAgentProgress("test-agent");
218
+ expect(result.status).toBe("stuck");
219
+ });
220
+
221
+ it("should return active when activity time cannot be determined", () => {
222
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue("Normal output");
223
+ vi.mocked(spawnSync).mockReturnValue({
224
+ status: 1,
225
+ stdout: "",
226
+ stderr: "",
227
+ pid: 123,
228
+ signal: null,
229
+ output: [],
230
+ });
231
+
232
+ const result = checkAgentProgress("test-agent");
233
+ expect(result.status).toBe("active");
234
+ expect(result.details).toContain("Unable to determine");
235
+ });
236
+ });
237
+
238
+ describe("isProcessRunning", () => {
239
+ it("should return true for running process", () => {
240
+ // Current process is always running
241
+ const result = isProcessRunning(process.pid);
242
+ expect(result).toBe(true);
243
+ });
244
+
245
+ it("should return false for non-existent process", () => {
246
+ // Use a very high PID that's unlikely to exist
247
+ const result = isProcessRunning(999999999);
248
+ expect(result).toBe(false);
249
+ });
250
+ });
251
+
252
+ describe("findOrphanContainers", () => {
253
+ it("should return list of container IDs", () => {
254
+ vi.mocked(spawnSync).mockReturnValue({
255
+ status: 0,
256
+ stdout: "abc123\ndef456\nghi789",
257
+ stderr: "",
258
+ pid: 123,
259
+ signal: null,
260
+ output: [],
261
+ });
262
+
263
+ const result = findOrphanContainers("test-agent");
264
+ expect(result).toEqual(["abc123", "def456", "ghi789"]);
265
+ });
266
+
267
+ it("should return empty array when no containers found", () => {
268
+ vi.mocked(spawnSync).mockReturnValue({
269
+ status: 0,
270
+ stdout: "",
271
+ stderr: "",
272
+ pid: 123,
273
+ signal: null,
274
+ output: [],
275
+ });
276
+
277
+ const result = findOrphanContainers("test-agent");
278
+ expect(result).toEqual([]);
279
+ });
280
+
281
+ it("should return empty array on command failure", () => {
282
+ vi.mocked(spawnSync).mockReturnValue({
283
+ status: 1,
284
+ stdout: "",
285
+ stderr: "docker not found",
286
+ pid: 123,
287
+ signal: null,
288
+ output: [],
289
+ });
290
+
291
+ const result = findOrphanContainers("test-agent");
292
+ expect(result).toEqual([]);
293
+ });
294
+ });
295
+
296
+ describe("cleanupOrphanContainers", () => {
297
+ it("should remove found containers and return count", () => {
298
+ vi.mocked(spawnSync)
299
+ .mockReturnValueOnce({
300
+ // findOrphanContainers call
301
+ status: 0,
302
+ stdout: "abc123\ndef456",
303
+ stderr: "",
304
+ pid: 123,
305
+ signal: null,
306
+ output: [],
307
+ })
308
+ .mockReturnValue({
309
+ // docker rm calls
310
+ status: 0,
311
+ stdout: "",
312
+ stderr: "",
313
+ pid: 123,
314
+ signal: null,
315
+ output: [],
316
+ });
317
+
318
+ const result = cleanupOrphanContainers("test-agent");
319
+ expect(result).toBe(2);
320
+ });
321
+
322
+ it("should return 0 when no orphan containers", () => {
323
+ vi.mocked(spawnSync).mockReturnValue({
324
+ status: 0,
325
+ stdout: "",
326
+ stderr: "",
327
+ pid: 123,
328
+ signal: null,
329
+ output: [],
330
+ });
331
+
332
+ const result = cleanupOrphanContainers("test-agent");
333
+ expect(result).toBe(0);
334
+ });
335
+ });
336
+
337
+ describe("sendNudge", () => {
338
+ it("should send nudge message to tmux session", () => {
339
+ vi.mocked(execSync).mockReturnValue(Buffer.from(""));
340
+
341
+ const result = sendNudge("test-agent", "Please continue working");
342
+ expect(result).toBe(true);
343
+ expect(execSync).toHaveBeenCalledWith(
344
+ expect.stringContaining("tmux send-keys"),
345
+ expect.any(Object),
346
+ );
347
+ });
348
+
349
+ it("should return false when tmux command fails", () => {
350
+ vi.mocked(execSync).mockImplementation(() => {
351
+ throw new Error("tmux session not found");
352
+ });
353
+
354
+ const result = sendNudge("test-agent", "Continue");
355
+ expect(result).toBe(false);
356
+ });
357
+
358
+ it("should escape quotes in message", () => {
359
+ vi.mocked(execSync).mockReturnValue(Buffer.from(""));
360
+
361
+ sendNudge("test-agent", 'Message with "quotes"');
362
+ expect(execSync).toHaveBeenCalledWith(
363
+ expect.stringContaining('\\"quotes\\"'),
364
+ expect.any(Object),
365
+ );
366
+ });
367
+ });
368
+ });
package/src/cli/attach.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { attachSession, sessionExists, getSessionName } from "../core/tmux.js";
1
+ import { execSync } from "node:child_process";
2
2
  import pc from "picocolors";
3
+ import { getAgentState } from "../config/loader.js";
4
+ import { attachSession, getSessionName, sessionExists } from "../core/tmux.js";
3
5
 
4
6
  export function attach(name: string): void {
5
7
  if (!name) {
@@ -7,6 +9,25 @@ export function attach(name: string): void {
7
9
  process.exit(1);
8
10
  }
9
11
 
12
+ // Check if this is a sandbox agent
13
+ const localAgent = getAgentState(name);
14
+
15
+ if (localAgent?.sandboxContainer) {
16
+ // Sandbox agent - attach via docker exec
17
+ console.log(`Attaching to sandbox container ${localAgent.sandboxContainer}...`);
18
+ console.log(pc.dim("Detach with: Ctrl+B, D\n"));
19
+
20
+ try {
21
+ execSync(`docker exec -it ${localAgent.sandboxContainer} agentmesh attach ${name}`, {
22
+ stdio: "inherit",
23
+ });
24
+ } catch {
25
+ // execSync throws on non-zero exit, but that's expected when detaching
26
+ }
27
+ return;
28
+ }
29
+
30
+ // Host agent - attach via tmux
10
31
  const sessionName = getSessionName(name);
11
32
 
12
33
  if (!sessionExists(sessionName)) {
package/src/cli/build.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { spawnSync, execSync } from "node:child_process";
1
+ import { execSync, spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import pc from "picocolors";
4
4
 
@@ -14,7 +14,9 @@ function findProjectRoot(): string {
14
14
  dir = path.dirname(dir);
15
15
  }
16
16
  }
17
- throw new Error("Could not find AgentMesh project root. Make sure you're in the agentmesh repository.");
17
+ throw new Error(
18
+ "Could not find AgentMesh project root. Make sure you're in the agentmesh repository.",
19
+ );
18
20
  }
19
21
 
20
22
  export interface BuildOptions {
@@ -128,8 +130,14 @@ async function buildDocker(projectRoot: string, options: BuildOptions): Promise<
128
130
  console.log(pc.bold("Built images:"));
129
131
  const listResult = spawnSync(
130
132
  "docker",
131
- ["images", "--filter", "reference=*agentmesh*", "--format", "{{.Repository}}:{{.Tag}}\t{{.Size}}"],
132
- { encoding: "utf-8" }
133
+ [
134
+ "images",
135
+ "--filter",
136
+ "reference=*agentmesh*",
137
+ "--format",
138
+ "{{.Repository}}:{{.Tag}}\t{{.Size}}",
139
+ ],
140
+ { encoding: "utf-8" },
133
141
  );
134
142
  if (listResult.stdout) {
135
143
  console.log(pc.dim(listResult.stdout));
@@ -1,6 +1,5 @@
1
1
  import pc from "picocolors";
2
2
  import { getAgentState, loadState } from "../config/loader.js";
3
- import { extractHandoffContext, formatHandoffContextSummary } from "../context/handoff.js";
4
3
  import {
5
4
  CONTEXT_DIR,
6
5
  deleteContext,
package/src/cli/deploy.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { spawnSync, execSync } from "node:child_process";
1
+ import { execSync, spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import pc from "picocolors";
4
4
 
@@ -14,7 +14,9 @@ function findProjectRoot(): string {
14
14
  dir = path.dirname(dir);
15
15
  }
16
16
  }
17
- throw new Error("Could not find AgentMesh project root. Make sure you're in the agentmesh repository.");
17
+ throw new Error(
18
+ "Could not find AgentMesh project root. Make sure you're in the agentmesh repository.",
19
+ );
18
20
  }
19
21
 
20
22
  export type DeployEnvironment = "dev" | "staging" | "prod";
@@ -51,10 +53,10 @@ export async function deploy(options: DeployOptions): Promise<void> {
51
53
  // Step 1: Build
52
54
  if (!options.skipBuild) {
53
55
  console.log(pc.bold("Step 1: Building..."));
54
-
56
+
55
57
  const buildScript = path.join(projectRoot, "scripts", "build.sh");
56
58
  const buildArgs = [buildScript, options.environment];
57
-
59
+
58
60
  if (options.dryRun) {
59
61
  console.log(pc.dim(`Would run: ${buildArgs.join(" ")}`));
60
62
  } else {
@@ -76,7 +78,7 @@ export async function deploy(options: DeployOptions): Promise<void> {
76
78
  // Step 2: Push to registry
77
79
  if (!options.skipPush) {
78
80
  console.log(pc.bold("Step 2: Pushing to registry..."));
79
-
81
+
80
82
  // Get the image tag from git commit
81
83
  let tag: string;
82
84
  try {
package/src/cli/index.ts CHANGED
@@ -10,7 +10,7 @@ import { contextCmd } from "./context.js";
10
10
  import { deploy } from "./deploy.js";
11
11
  import { init } from "./init.js";
12
12
  import { list } from "./list.js";
13
- import { localUp, localDown, localStatus, localLogs } from "./local.js";
13
+ import { localDown, localLogs, localStatus, localUp } from "./local.js";
14
14
  import { logs } from "./logs.js";
15
15
  import { migrate } from "./migrate.js";
16
16
  import { nudge } from "./nudge.js";
@@ -57,6 +57,13 @@ program
57
57
  .option("--auto-setup", "Auto-clone repository for project assignments")
58
58
  .option("--serve", "Run opencode serve instead of tmux TUI (for Integration Service)")
59
59
  .option("--serve-port <port>", "Port for opencode serve (default: 3001)", "3001")
60
+ .option("--sandbox", "Run agent in Docker sandbox container with filesystem isolation")
61
+ .option(
62
+ "--sandbox-image <image>",
63
+ "Docker image for sandbox (default: agentmesh/agent-sandbox:latest)",
64
+ )
65
+ .option("--sandbox-cpu <limit>", "CPU limit for sandbox (default: 1)")
66
+ .option("--sandbox-memory <limit>", "Memory limit for sandbox (default: 2g)")
60
67
  .action(async (options) => {
61
68
  try {
62
69
  // Parse serve port as number
package/src/cli/init.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as readline from "node:readline";
2
- import { loadConfig, saveConfig, createDefaultConfig } from "../config/loader.js";
3
- import type { Config } from "../config/schema.js";
4
2
  import pc from "picocolors";
3
+ import { createDefaultConfig, loadConfig, saveConfig } from "../config/loader.js";
4
+ import type { Config } from "../config/schema.js";
5
5
 
6
6
  function question(rl: readline.Interface, prompt: string): Promise<string> {
7
7
  return new Promise((resolve) => {
@@ -40,7 +40,7 @@ export async function init(): Promise<void> {
40
40
  try {
41
41
  const apiKey = await question(
42
42
  rl,
43
- `API Key ${pc.dim("(from agentmeshhq.dev/settings/api-keys)")}: `
43
+ `API Key ${pc.dim("(from agentmeshhq.dev/settings/api-keys)")}: `,
44
44
  );
45
45
 
46
46
  if (!apiKey) {
@@ -48,25 +48,13 @@ export async function init(): Promise<void> {
48
48
  return;
49
49
  }
50
50
 
51
- const workspace = await question(
52
- rl,
53
- `Workspace ${pc.dim("(default: agentmesh)")}: `
54
- );
51
+ const workspace = await question(rl, `Workspace ${pc.dim("(default: agentmesh)")}: `);
55
52
 
56
- const command = await question(
57
- rl,
58
- `Default command ${pc.dim("(default: opencode)")}: `
59
- );
53
+ const command = await question(rl, `Default command ${pc.dim("(default: opencode)")}: `);
60
54
 
61
- const model = await question(
62
- rl,
63
- `Default model ${pc.dim("(default: claude-sonnet-4)")}: `
64
- );
55
+ const model = await question(rl, `Default model ${pc.dim("(default: claude-sonnet-4)")}: `);
65
56
 
66
- const config: Config = createDefaultConfig(
67
- apiKey.trim(),
68
- workspace.trim() || "agentmesh"
69
- );
57
+ const config: Config = createDefaultConfig(apiKey.trim(), workspace.trim() || "agentmesh");
70
58
 
71
59
  if (command.trim()) {
72
60
  config.defaults.command = command.trim();
package/src/cli/list.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { loadState, loadConfig } from "../config/loader.js";
2
- import { sessionExists, getSessionName, getSessionInfo } from "../core/tmux.js";
3
- import { checkInbox, fetchAssignments } from "../core/registry.js";
4
1
  import pc from "picocolors";
2
+ import { loadConfig, loadState } from "../config/loader.js";
3
+ import { checkInbox, fetchAssignments } from "../core/registry.js";
4
+ import { getSessionInfo, getSessionName, sessionExists } from "../core/tmux.js";
5
5
 
6
6
  export async function list(): Promise<void> {
7
7
  const state = loadState();
@@ -15,7 +15,7 @@ export async function list(): Promise<void> {
15
15
 
16
16
  console.log(pc.bold("Running Agents:\n"));
17
17
  console.log(
18
- `${"NAME".padEnd(20)} ${"STATUS".padEnd(10)} ${"SESSION".padEnd(25)} ${"PENDING ID".padEnd(18)} ${"WORKDIR".padEnd(38)} ${"PROJECT"}`
18
+ `${"NAME".padEnd(20)} ${"STATUS".padEnd(10)} ${"SESSION".padEnd(25)} ${"PENDING ID".padEnd(18)} ${"WORKDIR".padEnd(38)} ${"PROJECT"}`,
19
19
  );
20
20
  console.log("-".repeat(140));
21
21
 
@@ -35,11 +35,7 @@ export async function list(): Promise<void> {
35
35
  // Try to check inbox if we have a token
36
36
  if (config && agent.token) {
37
37
  try {
38
- const items = await checkInbox(
39
- config.hubUrl,
40
- config.workspace,
41
- agent.token
42
- );
38
+ const items = await checkInbox(config.hubUrl, config.workspace, agent.token);
43
39
  if (items.length > 0) {
44
40
  const firstId = items[0]?.id || "-";
45
41
  pendingId = pc.yellow(items.length === 1 ? firstId : `${firstId} (+${items.length - 1})`);
@@ -62,7 +58,7 @@ export async function list(): Promise<void> {
62
58
  const workdir = agent.workdir || "-";
63
59
 
64
60
  console.log(
65
- `${agent.name.padEnd(20)} ${status.padEnd(19)} ${sessionName.padEnd(25)} ${pendingId.padEnd(18)} ${workdir.padEnd(38)} ${assignedProject}`
61
+ `${agent.name.padEnd(20)} ${status.padEnd(19)} ${sessionName.padEnd(25)} ${pendingId.padEnd(18)} ${workdir.padEnd(38)} ${assignedProject}`,
66
62
  );
67
63
 
68
64
  if (command) {
package/src/cli/local.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn, execSync, spawnSync } from "node:child_process";
1
+ import { execSync, spawn, spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import pc from "picocolors";
4
4
 
@@ -24,14 +24,20 @@ function findProjectRoot(): string {
24
24
  dir = path.dirname(dir);
25
25
  }
26
26
  }
27
- throw new Error("Could not find AgentMesh project root. Make sure you're in the agentmesh repository.");
27
+ throw new Error(
28
+ "Could not find AgentMesh project root. Make sure you're in the agentmesh repository.",
29
+ );
28
30
  }
29
31
 
30
- function getContainerStatus(containerName: string): { running: boolean; healthy: boolean; port?: string } {
32
+ function getContainerStatus(containerName: string): {
33
+ running: boolean;
34
+ healthy: boolean;
35
+ port?: string;
36
+ } {
31
37
  try {
32
38
  const result = execSync(
33
39
  `docker inspect --format='{{.State.Running}}:{{.State.Health.Status}}' ${containerName} 2>/dev/null`,
34
- { encoding: "utf-8" }
40
+ { encoding: "utf-8" },
35
41
  ).trim();
36
42
  const [running, health] = result.split(":");
37
43
  return {
@@ -45,10 +51,9 @@ function getContainerStatus(containerName: string): { running: boolean; healthy:
45
51
 
46
52
  function getContainerPort(containerName: string, internalPort: number): string | null {
47
53
  try {
48
- const result = execSync(
49
- `docker port ${containerName} ${internalPort} 2>/dev/null | head -1`,
50
- { encoding: "utf-8" }
51
- ).trim();
54
+ const result = execSync(`docker port ${containerName} ${internalPort} 2>/dev/null | head -1`, {
55
+ encoding: "utf-8",
56
+ }).trim();
52
57
  // Format: 0.0.0.0:5432 -> 5432
53
58
  const match = result.match(/:(\d+)$/);
54
59
  return match ? match[1] : null;
@@ -119,7 +124,7 @@ export async function localStatus(): Promise<void> {
119
124
 
120
125
  console.log(pc.bold("AgentMesh Local Stack Status"));
121
126
  console.log();
122
- console.log(pc.dim("Service".padEnd(15) + "Status".padEnd(12) + "Health".padEnd(10) + "Port"));
127
+ console.log(pc.dim(`${"Service".padEnd(15) + "Status".padEnd(12) + "Health".padEnd(10)}Port`));
123
128
  console.log(pc.dim("-".repeat(50)));
124
129
 
125
130
  let anyRunning = false;
@@ -137,7 +142,7 @@ export async function localStatus(): Promise<void> {
137
142
  const portText = port ? pc.cyan(port) : pc.dim("-");
138
143
 
139
144
  console.log(
140
- `${service.name.padEnd(15)}${statusText.padEnd(20)}${healthText.padEnd(18)}${portText}`
145
+ `${service.name.padEnd(15)}${statusText.padEnd(20)}${healthText.padEnd(18)}${portText}`,
141
146
  );
142
147
 
143
148
  if (status.running) anyRunning = true;
@@ -153,13 +158,17 @@ export async function localStatus(): Promise<void> {
153
158
  console.log();
154
159
  console.log(pc.bold("Commands:"));
155
160
  console.log(` Stop: ${pc.cyan("agentmesh local down")}`);
156
- console.log(` Logs: ${pc.cyan("docker compose -f docker/docker-compose.local.yml logs -f")}`);
161
+ console.log(
162
+ ` Logs: ${pc.cyan("docker compose -f docker/docker-compose.local.yml logs -f")}`,
163
+ );
157
164
  } else {
158
165
  console.log(pc.dim("No services running. Start with: agentmesh local up"));
159
166
  }
160
167
  }
161
168
 
162
- export async function localLogs(options: { follow?: boolean; service?: string } = {}): Promise<void> {
169
+ export async function localLogs(
170
+ options: { follow?: boolean; service?: string } = {},
171
+ ): Promise<void> {
163
172
  const projectRoot = findProjectRoot();
164
173
  const composePath = path.join(projectRoot, COMPOSE_FILE);
165
174