@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.
- package/dist/__tests__/loader.test.js +44 -1
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/sandbox.test.d.ts +1 -0
- package/dist/__tests__/sandbox.test.js +362 -0
- package/dist/__tests__/sandbox.test.js.map +1 -0
- package/dist/__tests__/watchdog.test.d.ts +1 -0
- package/dist/__tests__/watchdog.test.js +290 -0
- package/dist/__tests__/watchdog.test.js.map +1 -0
- package/dist/cli/attach.js +20 -1
- package/dist/cli/attach.js.map +1 -1
- package/dist/cli/build.js +8 -2
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/context.js.map +1 -1
- package/dist/cli/deploy.js +1 -1
- package/dist/cli/deploy.js.map +1 -1
- package/dist/cli/inbox.d.ts +5 -0
- package/dist/cli/inbox.js +123 -0
- package/dist/cli/inbox.js.map +1 -0
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/issue.d.ts +42 -0
- package/dist/cli/issue.js +297 -0
- package/dist/cli/issue.js.map +1 -0
- package/dist/cli/list.js +3 -3
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/local.js +5 -3
- package/dist/cli/local.js.map +1 -1
- package/dist/cli/migrate.js +1 -1
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/nudge.js +16 -3
- package/dist/cli/nudge.js.map +1 -1
- package/dist/cli/ready.d.ts +5 -0
- package/dist/cli/ready.js +131 -0
- package/dist/cli/ready.js.map +1 -0
- package/dist/cli/restart.js.map +1 -1
- package/dist/cli/slack.js +1 -1
- package/dist/cli/slack.js.map +1 -1
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +9 -0
- package/dist/cli/start.js.map +1 -1
- package/dist/cli/stop.js +13 -5
- package/dist/cli/stop.js.map +1 -1
- package/dist/cli/sync.d.ts +8 -0
- package/dist/cli/sync.js +154 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/cli/test.js +1 -1
- package/dist/cli/test.js.map +1 -1
- package/dist/cli/token.js +2 -2
- package/dist/cli/token.js.map +1 -1
- package/dist/config/loader.d.ts +5 -1
- package/dist/config/loader.js +27 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +13 -0
- package/dist/core/daemon.d.ts +50 -0
- package/dist/core/daemon.js +445 -11
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/injector.d.ts +2 -2
- package/dist/core/injector.js +23 -4
- package/dist/core/injector.js.map +1 -1
- package/dist/core/issue-cache.d.ts +44 -0
- package/dist/core/issue-cache.js +75 -0
- package/dist/core/issue-cache.js.map +1 -0
- package/dist/core/registry.d.ts +5 -0
- package/dist/core/registry.js +8 -1
- package/dist/core/registry.js.map +1 -1
- package/dist/core/runner.d.ts +1 -1
- package/dist/core/runner.js +23 -1
- package/dist/core/runner.js.map +1 -1
- package/dist/core/sandbox.d.ts +138 -0
- package/dist/core/sandbox.js +409 -0
- package/dist/core/sandbox.js.map +1 -0
- package/dist/core/tmux.d.ts +8 -0
- package/dist/core/tmux.js +28 -1
- package/dist/core/tmux.js.map +1 -1
- package/dist/core/watchdog.d.ts +41 -0
- package/dist/core/watchdog.js +198 -0
- package/dist/core/watchdog.js.map +1 -0
- package/dist/core/websocket.js +1 -1
- package/dist/core/websocket.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/loader.test.ts +52 -4
- package/src/__tests__/runner.test.ts +1 -2
- package/src/__tests__/sandbox.test.ts +435 -0
- package/src/__tests__/watchdog.test.ts +368 -0
- package/src/cli/attach.ts +22 -1
- package/src/cli/build.ts +12 -4
- package/src/cli/context.ts +0 -1
- package/src/cli/deploy.ts +7 -5
- package/src/cli/index.ts +8 -1
- package/src/cli/init.ts +7 -19
- package/src/cli/list.ts +6 -10
- package/src/cli/local.ts +21 -12
- package/src/cli/migrate.ts +6 -4
- package/src/cli/nudge.ts +29 -14
- package/src/cli/restart.ts +1 -1
- package/src/cli/slack.ts +16 -15
- package/src/cli/start.ts +14 -0
- package/src/cli/stop.ts +14 -5
- package/src/cli/test.ts +5 -3
- package/src/cli/token.ts +4 -4
- package/src/config/loader.ts +29 -2
- package/src/config/schema.ts +14 -0
- package/src/core/daemon.ts +540 -17
- package/src/core/injector.ts +27 -4
- package/src/core/registry.ts +14 -1
- package/src/core/runner.ts +26 -1
- package/src/core/sandbox.ts +550 -0
- package/src/core/tmux.ts +35 -2
- package/src/core/watchdog.ts +238 -0
- package/src/core/websocket.ts +2 -2
- 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 {
|
|
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 {
|
|
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(
|
|
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
|
-
[
|
|
132
|
-
|
|
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));
|
package/src/cli/context.ts
CHANGED
package/src/cli/deploy.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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 {
|
|
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 {
|
|
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(
|
|
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): {
|
|
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
|
-
|
|
50
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
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
|
|