@agentmeshhq/agent 0.1.13 → 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.js.map +1 -1
- 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/init.js +1 -1
- package/dist/cli/init.js.map +1 -1
- 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/restart.js.map +1 -1
- package/dist/cli/slack.js +1 -1
- package/dist/cli/slack.js.map +1 -1
- package/dist/cli/stop.js +13 -5
- package/dist/cli/stop.js.map +1 -1
- 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 +32 -1
- package/dist/core/daemon.js +362 -19
- 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/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 +11 -0
- package/dist/core/sandbox.js +34 -2
- package/dist/core/sandbox.js.map +1 -1
- 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 +1 -1
- 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/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/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 +439 -24
- package/src/core/injector.ts +27 -4
- package/src/core/runner.ts +26 -1
- package/src/core/sandbox.ts +47 -2
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/core/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,IAAI,CAAC;AAgB3B,MAAM,OAAO,cAAc;IACjB,EAAE,GAAqB,IAAI,CAAC;IAC5B,MAAM,CAAkB;IACxB,iBAAiB,GAAG,CAAC,CAAC;IACtB,oBAAoB,GAAG,EAAE,CAAC;IAC1B,cAAc,GAAG,IAAI,CAAC;IACtB,eAAe,GAAG,IAAI,CAAC;IAE/B,YAAY,MAAuB;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,EAAE,EAAE,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;YAC3C,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,UAAU,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAE9D,IAAI,CAAC,EAAE,GAAG,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC;QAE/B,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YACtB,IAAI,CAAC,iBAAiB,GAAG,CAAC,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE;YAC7B,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAmB,CAAC;gBAC5D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YAC/B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACvB,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC5B,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,iBAAiB;QACvB,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;YAC1B,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,iBAAiB,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YACxD,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;YAChD,OAAO;QACT,CAAC;QAED,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,GAAG,
|
|
1
|
+
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/core/websocket.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
export * from "./config/loader.js";
|
|
2
|
+
export * from "./config/schema.js";
|
|
1
3
|
export { AgentDaemon } from "./core/daemon.js";
|
|
2
|
-
export { AgentWebSocket } from "./core/websocket.js";
|
|
3
4
|
export { Heartbeat } from "./core/heartbeat.js";
|
|
4
|
-
export * from "./core/tmux.js";
|
|
5
|
-
export * from "./core/registry.js";
|
|
6
5
|
export * from "./core/injector.js";
|
|
7
|
-
export * from "./
|
|
8
|
-
export * from "./
|
|
6
|
+
export * from "./core/registry.js";
|
|
7
|
+
export * from "./core/tmux.js";
|
|
8
|
+
export { AgentWebSocket } from "./core/websocket.js";
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Re-export core modules for programmatic usage
|
|
2
|
+
export * from "./config/loader.js";
|
|
3
|
+
export * from "./config/schema.js";
|
|
2
4
|
export { AgentDaemon } from "./core/daemon.js";
|
|
3
|
-
export { AgentWebSocket } from "./core/websocket.js";
|
|
4
5
|
export { Heartbeat } from "./core/heartbeat.js";
|
|
5
|
-
export * from "./core/tmux.js";
|
|
6
|
-
export * from "./core/registry.js";
|
|
7
6
|
export * from "./core/injector.js";
|
|
8
|
-
export * from "./
|
|
9
|
-
export * from "./
|
|
7
|
+
export * from "./core/registry.js";
|
|
8
|
+
export * from "./core/tmux.js";
|
|
9
|
+
export { AgentWebSocket } from "./core/websocket.js";
|
|
10
10
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gDAAgD;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAEhD,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
3
|
import {
|
|
5
4
|
addAgentToState,
|
|
6
5
|
getAgentState,
|
|
7
6
|
loadConfig,
|
|
8
7
|
loadState,
|
|
9
8
|
removeAgentFromState,
|
|
10
|
-
|
|
11
|
-
saveState,
|
|
9
|
+
resetAgentRestartCount,
|
|
12
10
|
updateAgentInState,
|
|
13
11
|
} from "../config/loader.js";
|
|
14
12
|
import type { AgentState, Config, State } from "../config/schema.js";
|
|
@@ -187,5 +185,55 @@ describe("Config Loader", () => {
|
|
|
187
185
|
// writeFileSync should not be called since agent doesn't exist
|
|
188
186
|
// Actually it will be called but the state will be unchanged
|
|
189
187
|
});
|
|
188
|
+
|
|
189
|
+
it("should update restart count and status fields", () => {
|
|
190
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
191
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockState));
|
|
192
|
+
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
|
|
193
|
+
|
|
194
|
+
updateAgentInState("test-agent", {
|
|
195
|
+
restartCount: 2,
|
|
196
|
+
status: "stuck",
|
|
197
|
+
stuckSince: "2024-01-01T01:00:00Z",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1] as string;
|
|
201
|
+
const parsedState = JSON.parse(writtenContent);
|
|
202
|
+
expect(parsedState.agents[0].restartCount).toBe(2);
|
|
203
|
+
expect(parsedState.agents[0].status).toBe("stuck");
|
|
204
|
+
expect(parsedState.agents[0].stuckSince).toBe("2024-01-01T01:00:00Z");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("resetAgentRestartCount", () => {
|
|
209
|
+
it("should reset restart count and clear stuck status", () => {
|
|
210
|
+
const stuckState: State = {
|
|
211
|
+
agents: [
|
|
212
|
+
{
|
|
213
|
+
name: "test-agent",
|
|
214
|
+
agentId: "agent-123",
|
|
215
|
+
pid: 12345,
|
|
216
|
+
tmuxSession: "agentmesh-test-agent",
|
|
217
|
+
startedAt: "2024-01-01T00:00:00Z",
|
|
218
|
+
token: "test-token",
|
|
219
|
+
restartCount: 3,
|
|
220
|
+
status: "stuck",
|
|
221
|
+
stuckSince: "2024-01-01T01:00:00Z",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
227
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(stuckState));
|
|
228
|
+
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
|
|
229
|
+
|
|
230
|
+
resetAgentRestartCount("test-agent");
|
|
231
|
+
|
|
232
|
+
const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1] as string;
|
|
233
|
+
const parsedState = JSON.parse(writtenContent);
|
|
234
|
+
expect(parsedState.agents[0].restartCount).toBe(0);
|
|
235
|
+
expect(parsedState.agents[0].status).toBe("running");
|
|
236
|
+
expect(parsedState.agents[0].stuckSince).toBeUndefined();
|
|
237
|
+
});
|
|
190
238
|
});
|
|
191
239
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { DockerSandbox, type SandboxConfig, type SandboxMountPolicy } from "../core/sandbox.js";
|
|
5
5
|
|
|
6
6
|
// Mock child_process
|
|
@@ -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/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();
|