@agentmeshhq/agent 0.1.4 → 0.1.6

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 (40) hide show
  1. package/dist/__tests__/jwt.test.d.ts +1 -0
  2. package/dist/__tests__/jwt.test.js +83 -0
  3. package/dist/__tests__/jwt.test.js.map +1 -0
  4. package/dist/__tests__/loader.test.d.ts +1 -0
  5. package/dist/__tests__/loader.test.js +148 -0
  6. package/dist/__tests__/loader.test.js.map +1 -0
  7. package/dist/cli/index.js +37 -5
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/token.d.ts +1 -0
  10. package/dist/cli/token.js +146 -0
  11. package/dist/cli/token.js.map +1 -0
  12. package/dist/cli/whoami.d.ts +1 -0
  13. package/dist/cli/whoami.js +98 -0
  14. package/dist/cli/whoami.js.map +1 -0
  15. package/dist/config/loader.d.ts +2 -1
  16. package/dist/config/loader.js +13 -1
  17. package/dist/config/loader.js.map +1 -1
  18. package/dist/core/daemon.js +50 -6
  19. package/dist/core/daemon.js.map +1 -1
  20. package/dist/core/heartbeat.d.ts +8 -0
  21. package/dist/core/heartbeat.js +50 -1
  22. package/dist/core/heartbeat.js.map +1 -1
  23. package/dist/core/tmux.d.ts +7 -1
  24. package/dist/core/tmux.js +31 -2
  25. package/dist/core/tmux.js.map +1 -1
  26. package/dist/utils/jwt.d.ts +36 -0
  27. package/dist/utils/jwt.js +70 -0
  28. package/dist/utils/jwt.js.map +1 -0
  29. package/package.json +6 -3
  30. package/src/__tests__/jwt.test.ts +112 -0
  31. package/src/__tests__/loader.test.ts +191 -0
  32. package/src/cli/index.ts +38 -5
  33. package/src/cli/token.ts +188 -0
  34. package/src/cli/whoami.ts +113 -0
  35. package/src/config/loader.ts +20 -7
  36. package/src/core/daemon.ts +64 -18
  37. package/src/core/heartbeat.ts +62 -1
  38. package/src/core/tmux.ts +46 -9
  39. package/src/utils/jwt.ts +87 -0
  40. package/vitest.config.ts +12 -0
@@ -0,0 +1,191 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import {
5
+ addAgentToState,
6
+ getAgentState,
7
+ loadConfig,
8
+ loadState,
9
+ removeAgentFromState,
10
+ saveConfig,
11
+ saveState,
12
+ updateAgentInState,
13
+ } from "../config/loader.js";
14
+ import type { AgentState, Config, State } from "../config/schema.js";
15
+
16
+ // Mock the file system
17
+ vi.mock("node:fs");
18
+
19
+ describe("Config Loader", () => {
20
+ const mockConfig: Config = {
21
+ hubUrl: "https://test.agentmesh.dev",
22
+ apiKey: "test-api-key",
23
+ workspace: "test-workspace",
24
+ defaults: {
25
+ command: "opencode",
26
+ model: "claude-sonnet-4",
27
+ },
28
+ agents: [],
29
+ };
30
+
31
+ const mockState: State = {
32
+ agents: [
33
+ {
34
+ name: "test-agent",
35
+ agentId: "agent-123",
36
+ pid: 12345,
37
+ tmuxSession: "agentmesh-test-agent",
38
+ startedAt: "2024-01-01T00:00:00Z",
39
+ token: "test-token",
40
+ },
41
+ ],
42
+ };
43
+
44
+ beforeEach(() => {
45
+ vi.resetAllMocks();
46
+ });
47
+
48
+ describe("loadConfig", () => {
49
+ it("should return null if config file does not exist", () => {
50
+ vi.mocked(fs.existsSync).mockReturnValue(false);
51
+ expect(loadConfig()).toBeNull();
52
+ });
53
+
54
+ it("should load and parse config file", () => {
55
+ vi.mocked(fs.existsSync).mockReturnValue(true);
56
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockConfig));
57
+
58
+ const config = loadConfig();
59
+ expect(config).toEqual(mockConfig);
60
+ });
61
+
62
+ it("should return null on parse error", () => {
63
+ vi.mocked(fs.existsSync).mockReturnValue(true);
64
+ vi.mocked(fs.readFileSync).mockReturnValue("invalid json");
65
+
66
+ expect(loadConfig()).toBeNull();
67
+ });
68
+ });
69
+
70
+ describe("loadState", () => {
71
+ it("should return empty state if file does not exist", () => {
72
+ vi.mocked(fs.existsSync).mockReturnValue(false);
73
+ expect(loadState()).toEqual({ agents: [] });
74
+ });
75
+
76
+ it("should load and parse state file", () => {
77
+ vi.mocked(fs.existsSync).mockReturnValue(true);
78
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockState));
79
+
80
+ const state = loadState();
81
+ expect(state).toEqual(mockState);
82
+ });
83
+ });
84
+
85
+ describe("addAgentToState", () => {
86
+ it("should add new agent to state", () => {
87
+ vi.mocked(fs.existsSync).mockReturnValue(true);
88
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ agents: [] }));
89
+ vi.mocked(fs.writeFileSync).mockImplementation(() => {});
90
+
91
+ const newAgent: AgentState = {
92
+ name: "new-agent",
93
+ agentId: "agent-456",
94
+ pid: 67890,
95
+ tmuxSession: "agentmesh-new-agent",
96
+ startedAt: "2024-01-02T00:00:00Z",
97
+ token: "new-token",
98
+ };
99
+
100
+ addAgentToState(newAgent);
101
+
102
+ expect(fs.writeFileSync).toHaveBeenCalled();
103
+ const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1] as string;
104
+ const parsedState = JSON.parse(writtenContent);
105
+ expect(parsedState.agents).toHaveLength(1);
106
+ expect(parsedState.agents[0].name).toBe("new-agent");
107
+ });
108
+
109
+ it("should replace existing agent with same name", () => {
110
+ vi.mocked(fs.existsSync).mockReturnValue(true);
111
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockState));
112
+ vi.mocked(fs.writeFileSync).mockImplementation(() => {});
113
+
114
+ const updatedAgent: AgentState = {
115
+ name: "test-agent",
116
+ agentId: "agent-789",
117
+ pid: 11111,
118
+ tmuxSession: "agentmesh-test-agent",
119
+ startedAt: "2024-01-03T00:00:00Z",
120
+ token: "updated-token",
121
+ };
122
+
123
+ addAgentToState(updatedAgent);
124
+
125
+ const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1] as string;
126
+ const parsedState = JSON.parse(writtenContent);
127
+ expect(parsedState.agents).toHaveLength(1);
128
+ expect(parsedState.agents[0].agentId).toBe("agent-789");
129
+ });
130
+ });
131
+
132
+ describe("removeAgentFromState", () => {
133
+ it("should remove agent from state", () => {
134
+ vi.mocked(fs.existsSync).mockReturnValue(true);
135
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockState));
136
+ vi.mocked(fs.writeFileSync).mockImplementation(() => {});
137
+
138
+ removeAgentFromState("test-agent");
139
+
140
+ const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1] as string;
141
+ const parsedState = JSON.parse(writtenContent);
142
+ expect(parsedState.agents).toHaveLength(0);
143
+ });
144
+ });
145
+
146
+ describe("getAgentState", () => {
147
+ it("should return agent by name", () => {
148
+ vi.mocked(fs.existsSync).mockReturnValue(true);
149
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockState));
150
+
151
+ const agent = getAgentState("test-agent");
152
+ expect(agent).toBeDefined();
153
+ expect(agent?.agentId).toBe("agent-123");
154
+ });
155
+
156
+ it("should return undefined for non-existent agent", () => {
157
+ vi.mocked(fs.existsSync).mockReturnValue(true);
158
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockState));
159
+
160
+ const agent = getAgentState("non-existent");
161
+ expect(agent).toBeUndefined();
162
+ });
163
+ });
164
+
165
+ describe("updateAgentInState", () => {
166
+ it("should update agent fields", () => {
167
+ vi.mocked(fs.existsSync).mockReturnValue(true);
168
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockState));
169
+ vi.mocked(fs.writeFileSync).mockImplementation(() => {});
170
+
171
+ updateAgentInState("test-agent", { token: "new-token-123" });
172
+
173
+ const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1] as string;
174
+ const parsedState = JSON.parse(writtenContent);
175
+ expect(parsedState.agents[0].token).toBe("new-token-123");
176
+ // Other fields should remain unchanged
177
+ expect(parsedState.agents[0].agentId).toBe("agent-123");
178
+ });
179
+
180
+ it("should do nothing for non-existent agent", () => {
181
+ vi.mocked(fs.existsSync).mockReturnValue(true);
182
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockState));
183
+ vi.mocked(fs.writeFileSync).mockImplementation(() => {});
184
+
185
+ updateAgentInState("non-existent", { token: "new-token" });
186
+
187
+ // writeFileSync should not be called since agent doesn't exist
188
+ // Actually it will be called but the state will be unchanged
189
+ });
190
+ });
191
+ });
package/src/cli/index.ts CHANGED
@@ -1,20 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { createRequire } from "node:module";
3
4
  import { Command } from "commander";
5
+ import pc from "picocolors";
6
+ import { attach } from "./attach.js";
4
7
  import { init } from "./init.js";
5
- import { start } from "./start.js";
6
- import { stop } from "./stop.js";
7
8
  import { list } from "./list.js";
8
- import { attach } from "./attach.js";
9
9
  import { nudge } from "./nudge.js";
10
- import pc from "picocolors";
10
+ import { start } from "./start.js";
11
+ import { stop } from "./stop.js";
12
+ import { token } from "./token.js";
13
+ import { whoami } from "./whoami.js";
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const pkg = require("../../package.json") as { version: string };
11
17
 
12
18
  const program = new Command();
13
19
 
14
20
  program
15
21
  .name("agentmesh")
16
22
  .description("AgentMesh Agent Wrapper - Turn any AI assistant into a dispatchable agent")
17
- .version("0.1.4");
23
+ .version(pkg.version);
18
24
 
19
25
  program
20
26
  .command("init")
@@ -98,4 +104,31 @@ program
98
104
  }
99
105
  });
100
106
 
107
+ program
108
+ .command("whoami")
109
+ .description("Show current agent identity and status")
110
+ .argument("[name]", "Agent name (optional)")
111
+ .action(async (name) => {
112
+ try {
113
+ await whoami(name);
114
+ } catch (error) {
115
+ console.error(pc.red((error as Error).message));
116
+ process.exit(1);
117
+ }
118
+ });
119
+
120
+ program
121
+ .command("token")
122
+ .description("Manage agent tokens")
123
+ .argument("[action]", "Action: show (default), refresh, info")
124
+ .option("-n, --name <name>", "Agent name")
125
+ .action(async (action, options) => {
126
+ try {
127
+ await token(action || "show", options.name);
128
+ } catch (error) {
129
+ console.error(pc.red((error as Error).message));
130
+ process.exit(1);
131
+ }
132
+ });
133
+
101
134
  program.parse();
@@ -0,0 +1,188 @@
1
+ import pc from "picocolors";
2
+ import { loadConfig, loadState, updateAgentInState } from "../config/loader.js";
3
+ import { registerAgent } from "../core/registry.js";
4
+ import { getSessionName, sessionExists, updateSessionEnvironment } from "../core/tmux.js";
5
+ import {
6
+ decodeToken,
7
+ getTokenExpiry,
8
+ getTokenTimeRemaining,
9
+ isTokenExpired,
10
+ } from "../utils/jwt.js";
11
+
12
+ export async function token(action: string, agentName?: string): Promise<void> {
13
+ const config = loadConfig();
14
+
15
+ if (!config) {
16
+ console.log(pc.red("No config found. Run 'agentmesh init' first."));
17
+ process.exit(1);
18
+ }
19
+
20
+ // Check if running inside an agent session
21
+ const envAgentId = process.env.AGENTMESH_AGENT_ID;
22
+ const envToken = process.env.AGENT_TOKEN;
23
+
24
+ switch (action) {
25
+ case "show":
26
+ await showToken(config, agentName, envAgentId, envToken);
27
+ break;
28
+ case "refresh":
29
+ await refreshToken(config, agentName, envAgentId);
30
+ break;
31
+ case "info":
32
+ await tokenInfo(config, agentName, envAgentId, envToken);
33
+ break;
34
+ default:
35
+ console.log(pc.red(`Unknown action: ${action}`));
36
+ console.log(pc.dim("Available actions: show, refresh, info"));
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ async function showToken(
42
+ config: ReturnType<typeof loadConfig>,
43
+ agentName?: string,
44
+ envAgentId?: string,
45
+ envToken?: string,
46
+ ): Promise<void> {
47
+ // If running as agent, show env token
48
+ if (envToken && !agentName) {
49
+ console.log(envToken);
50
+ return;
51
+ }
52
+
53
+ // Find agent in state
54
+ const state = loadState();
55
+ const agent = agentName ? state.agents.find((a) => a.name === agentName) : state.agents[0];
56
+
57
+ if (!agent) {
58
+ if (agentName) {
59
+ console.log(pc.red(`Agent "${agentName}" not found.`));
60
+ } else {
61
+ console.log(pc.red("No agents registered."));
62
+ }
63
+ process.exit(1);
64
+ }
65
+
66
+ if (!agent.token) {
67
+ console.log(pc.red(`No token found for agent "${agent.name}".`));
68
+ console.log(pc.dim("Run 'agentmesh token refresh' to get a new token."));
69
+ process.exit(1);
70
+ }
71
+
72
+ console.log(agent.token);
73
+ }
74
+
75
+ async function refreshToken(
76
+ config: ReturnType<typeof loadConfig>,
77
+ agentName?: string,
78
+ envAgentId?: string,
79
+ ): Promise<void> {
80
+ if (!config) {
81
+ process.exit(1);
82
+ }
83
+
84
+ const state = loadState();
85
+
86
+ // Find the agent to refresh
87
+ const agent = agentName
88
+ ? state.agents.find((a) => a.name === agentName)
89
+ : envAgentId
90
+ ? state.agents.find((a) => a.agentId === envAgentId)
91
+ : state.agents[0];
92
+
93
+ if (!agent) {
94
+ console.log(pc.red("No agent found to refresh token for."));
95
+ process.exit(1);
96
+ }
97
+
98
+ console.log(pc.dim(`Refreshing token for "${agent.name}"...`));
99
+
100
+ try {
101
+ const registration = await registerAgent({
102
+ url: config.hubUrl,
103
+ apiKey: config.apiKey,
104
+ workspace: config.workspace,
105
+ agentId: agent.agentId,
106
+ agentName: agent.name,
107
+ model: "claude-sonnet-4", // Default model
108
+ });
109
+
110
+ // Update state
111
+ updateAgentInState(agent.name, {
112
+ token: registration.token,
113
+ });
114
+
115
+ // Update tmux environment if session exists
116
+ if (sessionExists(getSessionName(agent.name))) {
117
+ updateSessionEnvironment(agent.name, {
118
+ AGENT_TOKEN: registration.token,
119
+ AGENTMESH_AGENT_ID: registration.agentId,
120
+ });
121
+ console.log(pc.dim("Updated tmux session environment."));
122
+ }
123
+
124
+ console.log(pc.green("Token refreshed successfully."));
125
+
126
+ const expiry = getTokenExpiry(registration.token);
127
+ if (expiry) {
128
+ console.log(pc.dim(`New expiry: ${expiry.toISOString()}`));
129
+ }
130
+ } catch (error) {
131
+ console.log(pc.red(`Failed to refresh token: ${(error as Error).message}`));
132
+ process.exit(1);
133
+ }
134
+ }
135
+
136
+ async function tokenInfo(
137
+ config: ReturnType<typeof loadConfig>,
138
+ agentName?: string,
139
+ envAgentId?: string,
140
+ envToken?: string,
141
+ ): Promise<void> {
142
+ // Get token to inspect
143
+ let tokenToInspect: string | null = null;
144
+ let source = "unknown";
145
+
146
+ if (envToken && !agentName) {
147
+ tokenToInspect = envToken;
148
+ source = "environment";
149
+ } else {
150
+ const state = loadState();
151
+ const agent = agentName ? state.agents.find((a) => a.name === agentName) : state.agents[0];
152
+
153
+ if (agent?.token) {
154
+ tokenToInspect = agent.token;
155
+ source = `agent: ${agent.name}`;
156
+ }
157
+ }
158
+
159
+ if (!tokenToInspect) {
160
+ console.log(pc.red("No token found."));
161
+ process.exit(1);
162
+ }
163
+
164
+ const payload = decodeToken(tokenToInspect);
165
+ if (!payload) {
166
+ console.log(pc.red("Invalid token format."));
167
+ process.exit(1);
168
+ }
169
+
170
+ const expiry = getTokenExpiry(tokenToInspect);
171
+ const expired = isTokenExpired(tokenToInspect);
172
+ const remaining = getTokenTimeRemaining(tokenToInspect);
173
+
174
+ console.log(pc.bold("Token Info"));
175
+ console.log(` Source: ${pc.dim(source)}`);
176
+ console.log(` Agent ID: ${pc.cyan(payload.sub)}`);
177
+ console.log(` Type: ${pc.dim(payload.actorType)}`);
178
+ console.log(` Workspaces: ${pc.dim(payload.workspaceScopes.join(", "))}`);
179
+ console.log(` Issued: ${pc.dim(new Date(payload.iat * 1000).toISOString())}`);
180
+ console.log(` Expires: ${expiry ? pc.dim(expiry.toISOString()) : pc.red("unknown")}`);
181
+ console.log(` Status: ${expired ? pc.red("EXPIRED") : pc.green("VALID")}`);
182
+
183
+ if (!expired && remaining > 0) {
184
+ const days = Math.floor(remaining / (1000 * 60 * 60 * 24));
185
+ const hours = Math.floor((remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
186
+ console.log(` Remaining: ${pc.dim(`${days}d ${hours}h`)}`);
187
+ }
188
+ }
@@ -0,0 +1,113 @@
1
+ import pc from "picocolors";
2
+ import { loadConfig, loadState } from "../config/loader.js";
3
+ import { decodeToken, getTokenExpiry } from "../utils/jwt.js";
4
+
5
+ export async function whoami(agentName?: string): Promise<void> {
6
+ const config = loadConfig();
7
+
8
+ if (!config) {
9
+ console.log(pc.red("No config found. Run 'agentmesh init' first."));
10
+ process.exit(1);
11
+ }
12
+
13
+ const state = loadState();
14
+
15
+ // If no agent name provided, check environment or show all
16
+ if (!agentName) {
17
+ // Check if running as an agent (env vars set)
18
+ const envAgentId = process.env.AGENTMESH_AGENT_ID;
19
+ const envToken = process.env.AGENT_TOKEN;
20
+
21
+ if (envAgentId && envToken) {
22
+ // We're running inside an agent session
23
+ const expiry = getTokenExpiry(envToken);
24
+ const expiryStr = formatExpiry(expiry);
25
+
26
+ console.log(pc.bold("Current Agent"));
27
+ console.log(` ID: ${pc.cyan(envAgentId)}`);
28
+ console.log(` Workspace: ${pc.dim(config.workspace)}`);
29
+ console.log(` Token: ${expiryStr}`);
30
+ console.log(` Hub: ${pc.dim(config.hubUrl)}`);
31
+ return;
32
+ }
33
+
34
+ // Show all registered agents
35
+ if (state.agents.length === 0) {
36
+ console.log(pc.dim("No agents registered."));
37
+ console.log(pc.dim("Start an agent with: agentmesh start <name>"));
38
+ return;
39
+ }
40
+
41
+ console.log(pc.bold("Registered Agents"));
42
+ console.log();
43
+
44
+ for (const agent of state.agents) {
45
+ const expiry = agent.token ? getTokenExpiry(agent.token) : null;
46
+ const expiryStr = formatExpiry(expiry);
47
+ const running = agent.pid ? isProcessRunning(agent.pid) : false;
48
+
49
+ console.log(` ${pc.cyan(agent.name)}`);
50
+ console.log(` ID: ${pc.dim(agent.agentId)}`);
51
+ console.log(` Status: ${running ? pc.green("running") : pc.yellow("stopped")}`);
52
+ console.log(` Token: ${expiryStr}`);
53
+ console.log();
54
+ }
55
+ return;
56
+ }
57
+
58
+ // Show specific agent
59
+ const agent = state.agents.find((a) => a.name === agentName);
60
+
61
+ if (!agent) {
62
+ console.log(pc.red(`Agent "${agentName}" not found.`));
63
+ process.exit(1);
64
+ }
65
+
66
+ const expiry = agent.token ? getTokenExpiry(agent.token) : null;
67
+ const expiryStr = formatExpiry(expiry);
68
+ const running = agent.pid ? isProcessRunning(agent.pid) : false;
69
+
70
+ console.log(pc.bold(`Agent: ${agent.name}`));
71
+ console.log(` ID: ${pc.cyan(agent.agentId)}`);
72
+ console.log(` Workspace: ${pc.dim(config.workspace)}`);
73
+ console.log(` Status: ${running ? pc.green("running") : pc.yellow("stopped")}`);
74
+ console.log(` Token: ${expiryStr}`);
75
+ console.log(` Session: ${pc.dim(agent.tmuxSession || "none")}`);
76
+ console.log(` Started: ${pc.dim(agent.startedAt || "unknown")}`);
77
+ console.log(` Hub: ${pc.dim(config.hubUrl)}`);
78
+ }
79
+
80
+ function formatExpiry(expiry: Date | null): string {
81
+ if (!expiry) {
82
+ return pc.red("No token");
83
+ }
84
+
85
+ const now = new Date();
86
+ const diffMs = expiry.getTime() - now.getTime();
87
+
88
+ if (diffMs <= 0) {
89
+ return pc.red("Expired");
90
+ }
91
+
92
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
93
+ const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
94
+
95
+ if (diffDays > 1) {
96
+ return pc.green(`Valid (expires in ${diffDays} days)`);
97
+ } else if (diffDays === 1) {
98
+ return pc.yellow(`Valid (expires in 1 day)`);
99
+ } else if (diffHours > 0) {
100
+ return pc.yellow(`Valid (expires in ${diffHours} hours)`);
101
+ } else {
102
+ return pc.red(`Expiring soon`);
103
+ }
104
+ }
105
+
106
+ function isProcessRunning(pid: number): boolean {
107
+ try {
108
+ process.kill(pid, 0);
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
@@ -1,12 +1,12 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import {
4
- type Config,
5
- type State,
6
4
  type AgentState,
7
5
  CONFIG_PATH,
8
- STATE_PATH,
6
+ type Config,
9
7
  DEFAULT_CONFIG,
8
+ STATE_PATH,
9
+ type State,
10
10
  } from "./schema.js";
11
11
 
12
12
  export function ensureConfigDir(): void {
@@ -69,10 +69,23 @@ export function getAgentState(name: string): AgentState | undefined {
69
69
  return state.agents.find((a) => a.name === name);
70
70
  }
71
71
 
72
- export function createDefaultConfig(
73
- apiKey: string,
74
- workspace: string
75
- ): Config {
72
+ export function updateAgentInState(name: string, updates: Partial<AgentState>): void {
73
+ const state = loadState();
74
+ const agentIndex = state.agents.findIndex((a) => a.name === name);
75
+
76
+ if (agentIndex === -1) {
77
+ return;
78
+ }
79
+
80
+ state.agents[agentIndex] = {
81
+ ...state.agents[agentIndex],
82
+ ...updates,
83
+ };
84
+
85
+ saveState(state);
86
+ }
87
+
88
+ export function createDefaultConfig(apiKey: string, workspace: string): Config {
76
89
  return {
77
90
  ...DEFAULT_CONFIG,
78
91
  apiKey,