@agentmeshhq/agent 0.4.10 → 0.4.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 therajushahi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Unit tests for registerAgent() — verifies team_id is correctly
3
+ * included/excluded in the registration payload sent to the hub.
4
+ *
5
+ * This guards against the hub/CLI contract drift where team_id was
6
+ * required by the hub for worker agents but never sent by the CLI.
7
+ */
8
+ export {};
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Unit tests for registerAgent() — verifies team_id is correctly
3
+ * included/excluded in the registration payload sent to the hub.
4
+ *
5
+ * This guards against the hub/CLI contract drift where team_id was
6
+ * required by the hub for worker agents but never sent by the CLI.
7
+ */
8
+ import { afterEach, describe, expect, it, vi } from "vitest";
9
+ import { registerAgent } from "../core/registry.js";
10
+ const BASE_OPTIONS = {
11
+ url: "https://agentmeshhq.dev",
12
+ apiKey: "test-secret",
13
+ workspace: "agentmesh",
14
+ agentName: "test-agent",
15
+ model: "claude-sonnet-4-5",
16
+ };
17
+ function mockFetch(overrides = {}) {
18
+ const mock = vi.fn().mockResolvedValue({
19
+ ok: overrides.ok ?? true,
20
+ json: async () => ({
21
+ token: "eyJhbGciOiJIUzI1NiJ9.test.token",
22
+ agent_id: "agt_test123",
23
+ status: "registered",
24
+ ...(overrides.body ?? {}),
25
+ }),
26
+ text: async () => JSON.stringify(overrides.body ?? {}),
27
+ });
28
+ vi.stubGlobal("fetch", mock);
29
+ return mock;
30
+ }
31
+ describe("registerAgent — team_id contract", () => {
32
+ afterEach(() => {
33
+ vi.restoreAllMocks();
34
+ vi.unstubAllGlobals();
35
+ });
36
+ it("includes team_id in payload when provided", async () => {
37
+ const fetchMock = mockFetch();
38
+ await registerAgent({ ...BASE_OPTIONS, teamId: "team_abc123" });
39
+ expect(fetchMock).toHaveBeenCalledOnce();
40
+ const [, init] = fetchMock.mock.calls[0];
41
+ const body = JSON.parse(init.body);
42
+ expect(body.team_id).toBe("team_abc123");
43
+ });
44
+ it("omits team_id from payload when not provided", async () => {
45
+ const fetchMock = mockFetch();
46
+ await registerAgent({ ...BASE_OPTIONS });
47
+ const [, init] = fetchMock.mock.calls[0];
48
+ const body = JSON.parse(init.body);
49
+ expect(body).not.toHaveProperty("team_id");
50
+ });
51
+ it("omits team_id when explicitly undefined", async () => {
52
+ const fetchMock = mockFetch();
53
+ await registerAgent({ ...BASE_OPTIONS, teamId: undefined });
54
+ const [, init] = fetchMock.mock.calls[0];
55
+ const body = JSON.parse(init.body);
56
+ expect(body).not.toHaveProperty("team_id");
57
+ });
58
+ it("sends correct agent_type for worker agents", async () => {
59
+ const fetchMock = mockFetch();
60
+ await registerAgent({ ...BASE_OPTIONS, agentType: "worker", teamId: "team_abc123" });
61
+ const [, init] = fetchMock.mock.calls[0];
62
+ const body = JSON.parse(init.body);
63
+ expect(body.agent_type).toBe("worker");
64
+ expect(body.team_id).toBe("team_abc123");
65
+ });
66
+ it("does NOT send team_id for system agents even if teamId is set", async () => {
67
+ const fetchMock = mockFetch();
68
+ await registerAgent({ ...BASE_OPTIONS, agentType: "system", teamId: "team_abc123" });
69
+ const [, init] = fetchMock.mock.calls[0];
70
+ const body = JSON.parse(init.body);
71
+ expect(body.agent_type).toBe("system");
72
+ expect(body).not.toHaveProperty("team_id");
73
+ });
74
+ it("does NOT send team_id for autonomous agents even if teamId is set", async () => {
75
+ const fetchMock = mockFetch();
76
+ await registerAgent({ ...BASE_OPTIONS, agentType: "autonomous", teamId: "team_abc123" });
77
+ const [, init] = fetchMock.mock.calls[0];
78
+ const body = JSON.parse(init.body);
79
+ expect(body.agent_type).toBe("autonomous");
80
+ expect(body).not.toHaveProperty("team_id");
81
+ });
82
+ it("hits the correct endpoint", async () => {
83
+ const fetchMock = mockFetch();
84
+ await registerAgent({ ...BASE_OPTIONS, teamId: "team_abc123" });
85
+ const [url] = fetchMock.mock.calls[0];
86
+ expect(url).toBe("https://agentmeshhq.dev/api/v1/agents/register");
87
+ });
88
+ it("uses x-agentmesh-secret header for auth", async () => {
89
+ const fetchMock = mockFetch();
90
+ await registerAgent({ ...BASE_OPTIONS, apiKey: "my-secret-key", teamId: "team_abc123" });
91
+ const [, init] = fetchMock.mock.calls[0];
92
+ expect(init.headers["x-agentmesh-secret"]).toBe("my-secret-key");
93
+ });
94
+ it("throws when hub returns non-ok response", async () => {
95
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
96
+ ok: false,
97
+ text: async () => JSON.stringify({ error: "team_id is required for worker agent registration" }),
98
+ }));
99
+ await expect(registerAgent({ ...BASE_OPTIONS })).rejects.toThrow("Failed to register agent");
100
+ });
101
+ it("returns agentId and token from response", async () => {
102
+ mockFetch({ body: { token: "tok_abc", agent_id: "agt_xyz", status: "registered" } });
103
+ const result = await registerAgent({ ...BASE_OPTIONS, teamId: "team_abc123" });
104
+ expect(result.token).toBe("tok_abc");
105
+ expect(result.agentId).toBeDefined();
106
+ expect(result.status).toBe("registered");
107
+ });
108
+ });
109
+ //# sourceMappingURL=registry.register.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.register.test.js","sourceRoot":"","sources":["../../src/__tests__/registry.register.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tests for --team-id CLI option wiring:
3
+ * 1. CLI help exposes --team-id flag
4
+ * 2. buildSpawnArgs() includes/excludes --team-id correctly (pure, no spawn mocking needed)
5
+ * 3. Config persistence via upsertAgentConfig
6
+ * 4. Client-side validation: --worker without --team-id exits 1
7
+ * 5. system/autonomous agents do NOT get team_id in registration payload
8
+ */
9
+ export {};
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Tests for --team-id CLI option wiring:
3
+ * 1. CLI help exposes --team-id flag
4
+ * 2. buildSpawnArgs() includes/excludes --team-id correctly (pure, no spawn mocking needed)
5
+ * 3. Config persistence via upsertAgentConfig
6
+ * 4. Client-side validation: --worker without --team-id exits 1
7
+ * 5. system/autonomous agents do NOT get team_id in registration payload
8
+ */
9
+ import { execSync } from "node:child_process";
10
+ import { resolve } from "node:path";
11
+ import { afterEach, describe, expect, it, vi } from "vitest";
12
+ import { buildSpawnArgs } from "../cli/start.js";
13
+ const DIST_CLI = resolve(__dirname, "../../dist/cli/index.js");
14
+ // ---------------------------------------------------------------------------
15
+ // Spawn arg building — pure function, no mocking needed
16
+ // ---------------------------------------------------------------------------
17
+ describe("buildSpawnArgs — --team-id forwarding", () => {
18
+ it("includes --team-id when resolvedTeamId is provided", () => {
19
+ const args = buildSpawnArgs({ name: "forge-dev-1", worker: true }, { resolvedTeamId: "team_fd7i8ii2zjm2" });
20
+ expect(args).toContain("--team-id");
21
+ expect(args).toContain("team_fd7i8ii2zjm2");
22
+ const idx = args.indexOf("--team-id");
23
+ expect(args[idx + 1]).toBe("team_fd7i8ii2zjm2");
24
+ });
25
+ it("omits --team-id when resolvedTeamId is absent", () => {
26
+ const args = buildSpawnArgs({ name: "forge-dev-1", worker: true }, {});
27
+ expect(args).not.toContain("--team-id");
28
+ });
29
+ it("omits --team-id when resolvedTeamId is empty string", () => {
30
+ const args = buildSpawnArgs({ name: "forge-dev-1" }, { resolvedTeamId: "" });
31
+ expect(args).not.toContain("--team-id");
32
+ });
33
+ it("always includes --foreground and --name in spawn args", () => {
34
+ const args = buildSpawnArgs({ name: "my-agent" }, { resolvedTeamId: "team_abc" });
35
+ expect(args).toContain("start");
36
+ expect(args).toContain("--foreground");
37
+ expect(args).toContain("--name");
38
+ expect(args).toContain("my-agent");
39
+ });
40
+ it("includes --worker in spawn args when worker=true", () => {
41
+ const args = buildSpawnArgs({ name: "my-agent", worker: true }, { resolvedTeamId: "team_abc" });
42
+ expect(args).toContain("--worker");
43
+ });
44
+ it("preserves other options alongside --team-id", () => {
45
+ const args = buildSpawnArgs({ name: "my-agent", worker: true, autonomous: true, project: "PROJ" }, { resolvedTeamId: "team_abc", resolvedModel: "claude-sonnet-4-6" });
46
+ expect(args).toContain("--team-id");
47
+ expect(args).toContain("--autonomous");
48
+ expect(args).toContain("--project");
49
+ expect(args).toContain("--model");
50
+ });
51
+ });
52
+ // ---------------------------------------------------------------------------
53
+ // CLI binary — help text and client-side validation
54
+ // ---------------------------------------------------------------------------
55
+ describe("CLI binary — --team-id option", () => {
56
+ it("start --help exposes --team-id flag", () => {
57
+ let output = "";
58
+ try {
59
+ output = execSync(`node "${DIST_CLI}" start --help`, { encoding: "utf-8" });
60
+ }
61
+ catch (err) {
62
+ output = err.stdout ?? "";
63
+ }
64
+ expect(output).toMatch(/--team-id/);
65
+ });
66
+ it("start --worker without --team-id exits with code 1", () => {
67
+ // Provide a minimal config via env so the CLI can load it
68
+ // The validation should fire before any network call
69
+ let exitCode = 0;
70
+ try {
71
+ execSync(`node "${DIST_CLI}" start --name ci-test-agent --worker --foreground`, {
72
+ encoding: "utf-8",
73
+ env: {
74
+ ...process.env,
75
+ HOME: "/tmp/agentmesh-test-no-config",
76
+ },
77
+ });
78
+ }
79
+ catch (err) {
80
+ exitCode = err.status ?? 1;
81
+ }
82
+ // Should exit non-zero (either 1 from our validation or from missing config)
83
+ expect(exitCode).toBeGreaterThan(0);
84
+ });
85
+ });
86
+ // ---------------------------------------------------------------------------
87
+ // Config persistence
88
+ // ---------------------------------------------------------------------------
89
+ describe("start() — teamId config persistence", () => {
90
+ afterEach(() => {
91
+ vi.restoreAllMocks();
92
+ vi.unstubAllGlobals();
93
+ vi.resetModules();
94
+ });
95
+ it("persists teamId to agent config when provided", async () => {
96
+ const loader = await import("../config/loader.js");
97
+ const tmux = await import("../core/tmux.js");
98
+ vi.spyOn(loader, "loadConfig").mockReturnValue({
99
+ apiKey: "key",
100
+ workspace: "agentmesh",
101
+ hubUrl: "https://agentmeshhq.dev",
102
+ defaults: { command: "opencode", model: "claude-sonnet-4-5" },
103
+ agents: [],
104
+ });
105
+ vi.spyOn(loader, "getAgentState").mockReturnValue(undefined);
106
+ vi.spyOn(loader, "upsertAgentConfig").mockImplementation(() => { });
107
+ vi.spyOn(tmux, "getSessionName").mockReturnValue("agentmesh-test-agent");
108
+ vi.spyOn(tmux, "sessionExists").mockReturnValue(false);
109
+ const upsertSpy = vi.spyOn(loader, "upsertAgentConfig");
110
+ const { start } = await import("../cli/start.js");
111
+ await start({ name: "test-agent", worker: true, teamId: "team_fd7i8ii2zjm2" }).catch(() => { });
112
+ expect(upsertSpy).toHaveBeenCalledWith(expect.objectContaining({ teamId: "team_fd7i8ii2zjm2" }));
113
+ });
114
+ it("does not persist teamId when not provided and not in config", async () => {
115
+ vi.resetModules();
116
+ const loader = await import("../config/loader.js");
117
+ const tmux = await import("../core/tmux.js");
118
+ vi.spyOn(loader, "loadConfig").mockReturnValue({
119
+ apiKey: "key",
120
+ workspace: "agentmesh",
121
+ hubUrl: "https://agentmeshhq.dev",
122
+ defaults: { command: "opencode", model: "claude-sonnet-4-5" },
123
+ agents: [],
124
+ });
125
+ vi.spyOn(loader, "getAgentState").mockReturnValue(undefined);
126
+ vi.spyOn(loader, "upsertAgentConfig").mockImplementation(() => { });
127
+ vi.spyOn(tmux, "getSessionName").mockReturnValue("agentmesh-test-agent");
128
+ vi.spyOn(tmux, "sessionExists").mockReturnValue(false);
129
+ const upsertSpy = vi.spyOn(loader, "upsertAgentConfig");
130
+ const { start } = await import("../cli/start.js");
131
+ // Worker without team-id will hit client-side validation and exit(1), so catch
132
+ await start({ name: "test-agent", worker: true }).catch(() => { });
133
+ const call = upsertSpy.mock.calls[0]?.[0];
134
+ if (call) {
135
+ expect(call).not.toHaveProperty("teamId");
136
+ }
137
+ // If upsert wasn't called (exited before), that's also valid — validation fired first
138
+ });
139
+ it("resolves teamId from persisted config when not on CLI", async () => {
140
+ vi.resetModules();
141
+ const loader = await import("../config/loader.js");
142
+ const tmux = await import("../core/tmux.js");
143
+ vi.spyOn(loader, "loadConfig").mockReturnValue({
144
+ apiKey: "key",
145
+ workspace: "agentmesh",
146
+ hubUrl: "https://agentmeshhq.dev",
147
+ defaults: { command: "opencode", model: "claude-sonnet-4-5" },
148
+ agents: [{ name: "test-agent", command: "opencode", teamId: "team_from_config" }],
149
+ });
150
+ vi.spyOn(loader, "getAgentState").mockReturnValue(undefined);
151
+ vi.spyOn(loader, "upsertAgentConfig").mockImplementation(() => { });
152
+ vi.spyOn(tmux, "getSessionName").mockReturnValue("agentmesh-test-agent");
153
+ vi.spyOn(tmux, "sessionExists").mockReturnValue(false);
154
+ const upsertSpy = vi.spyOn(loader, "upsertAgentConfig");
155
+ const { start } = await import("../cli/start.js");
156
+ await start({ name: "test-agent", worker: true }).catch(() => { });
157
+ expect(upsertSpy).toHaveBeenCalledWith(expect.objectContaining({ teamId: "team_from_config" }));
158
+ });
159
+ });
160
+ //# sourceMappingURL=start-team-id.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start-team-id.test.js","sourceRoot":"","sources":["../../src/__tests__/start-team-id.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,162 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { PROJECT_WATCHER_RENEWAL_INTERVAL_SECONDS } from "../cli/watcher.js";
3
+ import { startProjectWatcherLoop } from "../core/project-watcher-loop.js";
4
+ describe("project watcher loop — tick timer", () => {
5
+ beforeEach(() => {
6
+ vi.useFakeTimers();
7
+ });
8
+ afterEach(() => {
9
+ vi.useRealTimers();
10
+ });
11
+ it("fires onTick at the configured interval", async () => {
12
+ const onTick = vi.fn().mockResolvedValue({
13
+ total_processed: 1,
14
+ total_nudged: 0,
15
+ total_escalated: 0,
16
+ });
17
+ const onRenew = vi
18
+ .fn()
19
+ .mockResolvedValue({ renewed: true, expires_at: "2026-03-28T00:00:00Z" });
20
+ const onLog = vi.fn();
21
+ const onError = vi.fn();
22
+ const onShutdown = vi.fn().mockResolvedValue(undefined);
23
+ const handle = startProjectWatcherLoop({
24
+ intervalSeconds: 60,
25
+ renewalIntervalSeconds: 180,
26
+ onTick,
27
+ onRenew,
28
+ onLog,
29
+ onError,
30
+ onShutdown,
31
+ });
32
+ expect(onTick).not.toHaveBeenCalled();
33
+ // Advance past first tick
34
+ await vi.advanceTimersByTimeAsync(60_001);
35
+ expect(onTick).toHaveBeenCalledTimes(1);
36
+ // Advance past second tick
37
+ await vi.advanceTimersByTimeAsync(60_001);
38
+ expect(onTick).toHaveBeenCalledTimes(2);
39
+ await handle.stop();
40
+ });
41
+ it("logs tick result counts after each tick", async () => {
42
+ const onTick = vi.fn().mockResolvedValue({
43
+ total_processed: 5,
44
+ total_nudged: 2,
45
+ total_escalated: 1,
46
+ });
47
+ const onRenew = vi
48
+ .fn()
49
+ .mockResolvedValue({ renewed: true, expires_at: "2026-03-28T00:00:00Z" });
50
+ const onLog = vi.fn();
51
+ const onError = vi.fn();
52
+ const onShutdown = vi.fn().mockResolvedValue(undefined);
53
+ const handle = startProjectWatcherLoop({
54
+ intervalSeconds: 60,
55
+ renewalIntervalSeconds: 180,
56
+ onTick,
57
+ onRenew,
58
+ onLog,
59
+ onError,
60
+ onShutdown,
61
+ });
62
+ await vi.advanceTimersByTimeAsync(60_001);
63
+ expect(onLog).toHaveBeenCalledWith("[tick] processed=5 nudged=2 escalated=1");
64
+ await handle.stop();
65
+ });
66
+ it("fires onRenew at the renewal interval (not with each tick)", async () => {
67
+ const onTick = vi.fn().mockResolvedValue({
68
+ total_processed: 0,
69
+ total_nudged: 0,
70
+ total_escalated: 0,
71
+ });
72
+ const onRenew = vi
73
+ .fn()
74
+ .mockResolvedValue({ renewed: true, expires_at: "2026-03-28T00:00:00Z" });
75
+ const onLog = vi.fn();
76
+ const onError = vi.fn();
77
+ const onShutdown = vi.fn().mockResolvedValue(undefined);
78
+ const handle = startProjectWatcherLoop({
79
+ intervalSeconds: 60,
80
+ renewalIntervalSeconds: 180,
81
+ onTick,
82
+ onRenew,
83
+ onLog,
84
+ onError,
85
+ onShutdown,
86
+ });
87
+ // 60s: tick fires once, no renewal yet
88
+ await vi.advanceTimersByTimeAsync(60_001);
89
+ expect(onTick).toHaveBeenCalledTimes(1);
90
+ expect(onRenew).toHaveBeenCalledTimes(0);
91
+ // 120s: second tick, still no renewal
92
+ await vi.advanceTimersByTimeAsync(60_001);
93
+ expect(onTick).toHaveBeenCalledTimes(2);
94
+ expect(onRenew).toHaveBeenCalledTimes(0);
95
+ // 180s: third tick AND first renewal
96
+ await vi.advanceTimersByTimeAsync(60_001);
97
+ expect(onTick).toHaveBeenCalledTimes(3);
98
+ expect(onRenew).toHaveBeenCalledTimes(1);
99
+ await handle.stop();
100
+ });
101
+ it("calls onShutdown and stops timers when renewal fails", async () => {
102
+ const onTick = vi.fn().mockResolvedValue({
103
+ total_processed: 0,
104
+ total_nudged: 0,
105
+ total_escalated: 0,
106
+ });
107
+ const onRenew = vi.fn().mockRejectedValue(new Error("network error"));
108
+ const onLog = vi.fn();
109
+ const onError = vi.fn();
110
+ const onShutdown = vi.fn().mockResolvedValue(undefined);
111
+ startProjectWatcherLoop({
112
+ intervalSeconds: 60,
113
+ renewalIntervalSeconds: 180,
114
+ onTick,
115
+ onRenew,
116
+ onLog,
117
+ onError,
118
+ onShutdown,
119
+ });
120
+ // Trigger renewal failure
121
+ await vi.advanceTimersByTimeAsync(180_001);
122
+ // Wait for the async renewal handler to complete
123
+ await vi.runAllTimersAsync();
124
+ expect(onRenew).toHaveBeenCalledTimes(1);
125
+ expect(onError).toHaveBeenCalledWith(expect.stringContaining("[renew] Lease renewal failed"));
126
+ expect(onShutdown).toHaveBeenCalledTimes(1);
127
+ });
128
+ it("stop() clears both timers and calls onShutdown once", async () => {
129
+ const onTick = vi.fn().mockResolvedValue({
130
+ total_processed: 0,
131
+ total_nudged: 0,
132
+ total_escalated: 0,
133
+ });
134
+ const onRenew = vi
135
+ .fn()
136
+ .mockResolvedValue({ renewed: true, expires_at: "2026-03-28T00:00:00Z" });
137
+ const onLog = vi.fn();
138
+ const onError = vi.fn();
139
+ const onShutdown = vi.fn().mockResolvedValue(undefined);
140
+ const handle = startProjectWatcherLoop({
141
+ intervalSeconds: 60,
142
+ renewalIntervalSeconds: 180,
143
+ onTick,
144
+ onRenew,
145
+ onLog,
146
+ onError,
147
+ onShutdown,
148
+ });
149
+ await handle.stop();
150
+ // Advance time well past intervals — no more ticks or renewals should fire
151
+ await vi.advanceTimersByTimeAsync(400_000);
152
+ expect(onTick).toHaveBeenCalledTimes(0);
153
+ expect(onRenew).toHaveBeenCalledTimes(0);
154
+ expect(onShutdown).toHaveBeenCalledTimes(1);
155
+ });
156
+ });
157
+ describe("project watcher renewal interval constant", () => {
158
+ it("renewal interval is 180s (60% of 300s TTL)", () => {
159
+ expect(PROJECT_WATCHER_RENEWAL_INTERVAL_SECONDS).toBe(180);
160
+ });
161
+ });
162
+ //# sourceMappingURL=watcher-runtime.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher-runtime.test.js","sourceRoot":"","sources":["../../src/__tests__/watcher-runtime.test.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -27,6 +27,7 @@ import { stop } from "./stop.js";
27
27
  import { sync } from "./sync.js";
28
28
  import { test } from "./test.js";
29
29
  import { token } from "./token.js";
30
+ import { pullWatcher, startProjectWatcher, startWatcher, stopWatcher, watcherStatus, } from "./watcher.js";
30
31
  import { resolvePO, whoami } from "./whoami.js";
31
32
  import { pauseWorkerAutomation, resumeWorkerAutomation } from "./worker.js";
32
33
  const require = createRequire(import.meta.url);
@@ -103,6 +104,7 @@ program
103
104
  .option("--sandbox-cpu <limit>", "CPU limit for sandbox (default: 1)")
104
105
  .option("--sandbox-memory <limit>", "Memory limit for sandbox (default: 2g)")
105
106
  .option("--autonomous", "Run agent in fully autonomous mode — skips all runtime permission prompts. Use with --worker for long-lived unattended agents.")
107
+ .option("--team-id <id>", "Team ID to register this worker agent with (required for --worker)")
106
108
  .action(async (options) => {
107
109
  try {
108
110
  if (options.project && options.workdir) {
@@ -264,6 +266,107 @@ claimsCommand
264
266
  process.exit(1);
265
267
  }
266
268
  });
269
+ const watcherCommand = program.command("watcher").description("Team watcher runtime controls");
270
+ watcherCommand
271
+ .command("start")
272
+ .description("Start watcher runtime for a team (--team-id) or project (--project-id)")
273
+ .option("--team-id <teamId>", "Team ID (team-scoped watcher)")
274
+ .option("--project-id <projectId>", "Project ID (project-scoped watcher with tick loop)")
275
+ .option("-n, --name <name>", "Agent name")
276
+ .option("-f, --foreground", "Run in foreground (blocking)")
277
+ .option("--interval <seconds>", "Tick interval in seconds for project watcher (default: 60)")
278
+ .option("--interval-seconds <seconds>", "Snapshot publish interval in seconds (default: 15)")
279
+ .option("--heartbeat-seconds <seconds>", "Lease renewal interval in seconds (default: 10)")
280
+ .option("--ttl-seconds <seconds>", "Lease TTL in seconds (default: 30)")
281
+ .action(async (options) => {
282
+ try {
283
+ if (options.projectId) {
284
+ await startProjectWatcher({
285
+ projectId: options.projectId,
286
+ name: options.name,
287
+ foreground: options.foreground,
288
+ intervalSeconds: options.interval ? parseInt(options.interval, 10) : undefined,
289
+ });
290
+ }
291
+ else if (options.teamId) {
292
+ await startWatcher({
293
+ teamId: options.teamId,
294
+ name: options.name,
295
+ foreground: options.foreground,
296
+ intervalSeconds: options.intervalSeconds
297
+ ? parseInt(options.intervalSeconds, 10)
298
+ : undefined,
299
+ heartbeatSeconds: options.heartbeatSeconds
300
+ ? parseInt(options.heartbeatSeconds, 10)
301
+ : undefined,
302
+ ttlSeconds: options.ttlSeconds ? parseInt(options.ttlSeconds, 10) : undefined,
303
+ });
304
+ }
305
+ else {
306
+ console.error(pc.red("Either --team-id or --project-id is required."));
307
+ process.exit(1);
308
+ }
309
+ }
310
+ catch (error) {
311
+ console.error(pc.red(error.message));
312
+ process.exit(1);
313
+ }
314
+ });
315
+ watcherCommand
316
+ .command("stop")
317
+ .description("Stop local watcher runtime for a team")
318
+ .requiredOption("--team-id <teamId>", "Team ID")
319
+ .action(async (options) => {
320
+ try {
321
+ await stopWatcher({ teamId: options.teamId });
322
+ }
323
+ catch (error) {
324
+ console.error(pc.red(error.message));
325
+ process.exit(1);
326
+ }
327
+ });
328
+ watcherCommand
329
+ .command("status")
330
+ .description("Show watcher status for a team")
331
+ .requiredOption("--team-id <teamId>", "Team ID")
332
+ .option("-n, --name <name>", "Agent name")
333
+ .option("--json", "Output JSON")
334
+ .option("--stale-after-seconds <seconds>", "Override stale threshold seconds")
335
+ .action(async (options) => {
336
+ try {
337
+ await watcherStatus({
338
+ teamId: options.teamId,
339
+ name: options.name,
340
+ json: options.json,
341
+ staleAfterSeconds: options.staleAfterSeconds
342
+ ? parseInt(options.staleAfterSeconds, 10)
343
+ : undefined,
344
+ });
345
+ }
346
+ catch (error) {
347
+ console.error(pc.red(error.message));
348
+ process.exit(1);
349
+ }
350
+ });
351
+ watcherCommand
352
+ .command("pull")
353
+ .description("Pull latest watcher snapshot for a team")
354
+ .requiredOption("--team-id <teamId>", "Team ID")
355
+ .option("-n, --name <name>", "Agent name")
356
+ .option("--json", "Output JSON")
357
+ .action(async (options) => {
358
+ try {
359
+ await pullWatcher({
360
+ teamId: options.teamId,
361
+ name: options.name,
362
+ json: options.json,
363
+ });
364
+ }
365
+ catch (error) {
366
+ console.error(pc.red(error.message));
367
+ process.exit(1);
368
+ }
369
+ });
267
370
  handoffCommand
268
371
  .command("reject")
269
372
  .description("Reject a handoff")