@agentmeshhq/agent 0.1.17 → 0.2.1

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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/dist/__tests__/orphan-process.test.d.ts +11 -0
  4. package/dist/__tests__/orphan-process.test.js +286 -0
  5. package/dist/__tests__/orphan-process.test.js.map +1 -0
  6. package/dist/__tests__/runner.test.js +16 -0
  7. package/dist/__tests__/runner.test.js.map +1 -1
  8. package/dist/__tests__/watchdog.test.js +138 -12
  9. package/dist/__tests__/watchdog.test.js.map +1 -1
  10. package/dist/cli/index.js +2 -1
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/start.d.ts +2 -1
  13. package/dist/cli/start.js +6 -3
  14. package/dist/cli/start.js.map +1 -1
  15. package/dist/cli/status.js +11 -0
  16. package/dist/cli/status.js.map +1 -1
  17. package/dist/cli/stop.js +7 -2
  18. package/dist/cli/stop.js.map +1 -1
  19. package/dist/config/schema.d.ts +4 -2
  20. package/dist/core/daemon/assignment-message.d.ts +12 -0
  21. package/dist/core/daemon/assignment-message.js +36 -0
  22. package/dist/core/daemon/assignment-message.js.map +1 -0
  23. package/dist/core/daemon/bootstrap.d.ts +35 -0
  24. package/dist/core/daemon/bootstrap.js +52 -0
  25. package/dist/core/daemon/bootstrap.js.map +1 -0
  26. package/dist/core/daemon/crash-log.d.ts +16 -0
  27. package/dist/core/daemon/crash-log.js +24 -0
  28. package/dist/core/daemon/crash-log.js.map +1 -0
  29. package/dist/core/daemon/health-policy.d.ts +21 -0
  30. package/dist/core/daemon/health-policy.js +32 -0
  31. package/dist/core/daemon/health-policy.js.map +1 -0
  32. package/dist/core/daemon/sandbox-config.d.ts +9 -0
  33. package/dist/core/daemon/sandbox-config.js +17 -0
  34. package/dist/core/daemon/sandbox-config.js.map +1 -0
  35. package/dist/core/daemon/state.d.ts +33 -0
  36. package/dist/core/daemon/state.js +77 -0
  37. package/dist/core/daemon/state.js.map +1 -0
  38. package/dist/core/daemon/tmux-session.d.ts +17 -0
  39. package/dist/core/daemon/tmux-session.js +34 -0
  40. package/dist/core/daemon/tmux-session.js.map +1 -0
  41. package/dist/core/daemon/workspace.d.ts +10 -0
  42. package/dist/core/daemon/workspace.js +51 -0
  43. package/dist/core/daemon/workspace.js.map +1 -0
  44. package/dist/core/daemon.d.ts +4 -7
  45. package/dist/core/daemon.js +143 -259
  46. package/dist/core/daemon.js.map +1 -1
  47. package/dist/core/injector.js +6 -0
  48. package/dist/core/injector.js.map +1 -1
  49. package/dist/core/registry.js +1 -1
  50. package/dist/core/registry.js.map +1 -1
  51. package/dist/core/runner/build.d.ts +9 -0
  52. package/dist/core/runner/build.js +53 -0
  53. package/dist/core/runner/build.js.map +1 -0
  54. package/dist/core/runner/detect.d.ts +5 -0
  55. package/dist/core/runner/detect.js +14 -0
  56. package/dist/core/runner/detect.js.map +1 -0
  57. package/dist/core/runner/index.d.ts +5 -0
  58. package/dist/core/runner/index.js +5 -0
  59. package/dist/core/runner/index.js.map +1 -0
  60. package/dist/core/runner/model.d.ts +5 -0
  61. package/dist/core/runner/model.js +7 -0
  62. package/dist/core/runner/model.js.map +1 -0
  63. package/dist/core/runner/opencode-models.d.ts +15 -0
  64. package/dist/core/runner/opencode-models.js +70 -0
  65. package/dist/core/runner/opencode-models.js.map +1 -0
  66. package/dist/core/runner/types.d.ts +19 -0
  67. package/dist/core/runner/types.js +8 -0
  68. package/dist/core/runner/types.js.map +1 -0
  69. package/dist/core/runner.d.ts +5 -47
  70. package/dist/core/runner.js +5 -167
  71. package/dist/core/runner.js.map +1 -1
  72. package/dist/core/tmux-runtime.d.ts +13 -0
  73. package/dist/core/tmux-runtime.js +72 -0
  74. package/dist/core/tmux-runtime.js.map +1 -0
  75. package/dist/core/tmux.d.ts +7 -1
  76. package/dist/core/tmux.js +75 -45
  77. package/dist/core/tmux.js.map +1 -1
  78. package/dist/core/watchdog.d.ts +18 -1
  79. package/dist/core/watchdog.js +78 -29
  80. package/dist/core/watchdog.js.map +1 -1
  81. package/package.json +30 -11
  82. package/dist/cli/inbox.d.ts +0 -5
  83. package/dist/cli/inbox.js +0 -123
  84. package/dist/cli/inbox.js.map +0 -1
  85. package/dist/cli/issue.d.ts +0 -42
  86. package/dist/cli/issue.js +0 -297
  87. package/dist/cli/issue.js.map +0 -1
  88. package/dist/cli/ready.d.ts +0 -5
  89. package/dist/cli/ready.js +0 -131
  90. package/dist/cli/ready.js.map +0 -1
  91. package/dist/cli/sync.d.ts +0 -8
  92. package/dist/cli/sync.js +0 -154
  93. package/dist/cli/sync.js.map +0 -1
  94. package/dist/core/issue-cache.d.ts +0 -44
  95. package/dist/core/issue-cache.js +0 -75
  96. package/dist/core/issue-cache.js.map +0 -1
  97. package/src/__tests__/context.test.ts +0 -464
  98. package/src/__tests__/injector.test.ts +0 -29
  99. package/src/__tests__/jwt.test.ts +0 -112
  100. package/src/__tests__/loader.test.ts +0 -239
  101. package/src/__tests__/runner.test.ts +0 -104
  102. package/src/__tests__/sandbox.test.ts +0 -435
  103. package/src/__tests__/watchdog.test.ts +0 -368
  104. package/src/cli/attach.ts +0 -22
  105. package/src/cli/build.ts +0 -145
  106. package/src/cli/config.ts +0 -148
  107. package/src/cli/context.ts +0 -231
  108. package/src/cli/deploy.ts +0 -155
  109. package/src/cli/index.ts +0 -375
  110. package/src/cli/init.ts +0 -75
  111. package/src/cli/list.ts +0 -70
  112. package/src/cli/local.ts +0 -183
  113. package/src/cli/logs.ts +0 -64
  114. package/src/cli/migrate.ts +0 -212
  115. package/src/cli/nudge.ts +0 -81
  116. package/src/cli/restart.ts +0 -59
  117. package/src/cli/slack.ts +0 -70
  118. package/src/cli/start.ts +0 -115
  119. package/src/cli/status.ts +0 -91
  120. package/src/cli/stop.ts +0 -48
  121. package/src/cli/test.ts +0 -143
  122. package/src/cli/token.ts +0 -188
  123. package/src/cli/whoami.ts +0 -142
  124. package/src/config/loader.ts +0 -121
  125. package/src/config/schema.ts +0 -68
  126. package/src/context/handoff.ts +0 -122
  127. package/src/context/index.ts +0 -8
  128. package/src/context/schema.ts +0 -111
  129. package/src/context/storage.ts +0 -197
  130. package/src/core/daemon.ts +0 -1308
  131. package/src/core/heartbeat.ts +0 -129
  132. package/src/core/injector.ts +0 -292
  133. package/src/core/registry.ts +0 -159
  134. package/src/core/runner.ts +0 -225
  135. package/src/core/sandbox.ts +0 -547
  136. package/src/core/session-id.ts +0 -111
  137. package/src/core/tmux.ts +0 -405
  138. package/src/core/watchdog.ts +0 -238
  139. package/src/core/websocket.ts +0 -94
  140. package/src/index.ts +0 -10
  141. package/src/utils/jwt.ts +0 -87
  142. package/tsconfig.json +0 -8
  143. package/vitest.config.ts +0 -12
@@ -1,129 +0,0 @@
1
- import { getTokenTimeRemaining, isTokenExpiringSoon } from "../utils/jwt.js";
2
-
3
- // Refresh token when less than 24 hours remain
4
- const TOKEN_REFRESH_THRESHOLD_MS = 24 * 60 * 60 * 1000;
5
-
6
- export interface HeartbeatConfig {
7
- url: string;
8
- token: string;
9
- intervalMs: number;
10
- agentName: string;
11
- agentId: string;
12
- apiKey: string;
13
- workspace: string;
14
- onError?: (error: Error) => void;
15
- onTokenRefresh?: (newToken: string) => void;
16
- /** Called periodically to save agent context (every N heartbeats) */
17
- onContextSave?: () => void;
18
- /** How many heartbeats between context saves (default: 5) */
19
- contextSaveFrequency?: number;
20
- }
21
-
22
- export class Heartbeat {
23
- private config: HeartbeatConfig;
24
- private currentToken: string;
25
- private intervalId: NodeJS.Timeout | null = null;
26
- private heartbeatCount = 0;
27
- private contextSaveFrequency: number;
28
-
29
- constructor(config: HeartbeatConfig) {
30
- this.config = config;
31
- this.currentToken = config.token;
32
- this.contextSaveFrequency = config.contextSaveFrequency ?? 5;
33
- }
34
-
35
- start(): void {
36
- if (this.intervalId) {
37
- return;
38
- }
39
-
40
- // Send initial heartbeat
41
- this.sendHeartbeat();
42
-
43
- // Schedule recurring heartbeats
44
- this.intervalId = setInterval(() => {
45
- this.sendHeartbeat();
46
- }, this.config.intervalMs);
47
- }
48
-
49
- stop(): void {
50
- if (this.intervalId) {
51
- clearInterval(this.intervalId);
52
- this.intervalId = null;
53
- }
54
- }
55
-
56
- getToken(): string {
57
- return this.currentToken;
58
- }
59
-
60
- private async sendHeartbeat(): Promise<void> {
61
- try {
62
- // Check if token needs refresh before sending heartbeat
63
- if (isTokenExpiringSoon(this.currentToken, TOKEN_REFRESH_THRESHOLD_MS)) {
64
- const remaining = getTokenTimeRemaining(this.currentToken);
65
- const hours = Math.floor(remaining / (1000 * 60 * 60));
66
- console.log(`Token expiring in ${hours} hours, refreshing...`);
67
- await this.refreshToken();
68
- }
69
-
70
- const response = await fetch(`${this.config.url}/api/v1/agents/heartbeat`, {
71
- method: "POST",
72
- headers: {
73
- Authorization: `Bearer ${this.currentToken}`,
74
- "Content-Type": "application/json",
75
- },
76
- body: JSON.stringify({}),
77
- });
78
-
79
- if (!response.ok) {
80
- // If unauthorized, try to refresh token
81
- if (response.status === 401) {
82
- console.log("Token rejected, attempting refresh...");
83
- await this.refreshToken();
84
- return;
85
- }
86
- throw new Error(`Heartbeat failed: ${response.status}`);
87
- }
88
-
89
- // Periodically save context
90
- this.heartbeatCount++;
91
- if (this.config.onContextSave && this.heartbeatCount % this.contextSaveFrequency === 0) {
92
- this.config.onContextSave();
93
- }
94
- } catch (error) {
95
- this.config.onError?.(error as Error);
96
- }
97
- }
98
-
99
- private async refreshToken(): Promise<void> {
100
- try {
101
- const response = await fetch(`${this.config.url}/api/v1/agents/register`, {
102
- method: "POST",
103
- headers: {
104
- "Content-Type": "application/json",
105
- "x-agentmesh-secret": this.config.apiKey,
106
- },
107
- body: JSON.stringify({
108
- agent_id: this.config.agentId,
109
- workspace: this.config.workspace,
110
- display_name: this.config.agentName,
111
- model: "claude-sonnet-4",
112
- }),
113
- });
114
-
115
- if (!response.ok) {
116
- throw new Error(`Token refresh failed: ${response.status}`);
117
- }
118
-
119
- const data = (await response.json()) as { token: string };
120
- this.currentToken = data.token;
121
-
122
- console.log("Token refreshed successfully");
123
- this.config.onTokenRefresh?.(data.token);
124
- } catch (error) {
125
- console.error("Failed to refresh token:", (error as Error).message);
126
- this.config.onError?.(error as Error);
127
- }
128
- }
129
- }
@@ -1,292 +0,0 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { formatHandoffContextSummary, parseHandoffContext } from "../context/handoff.js";
4
- import type { AgentContext } from "../context/schema.js";
5
- import type { InboxItem } from "./registry.js";
6
- import { sendKeys } from "./tmux.js";
7
- import type { WebSocketEvent } from "./websocket.js";
8
-
9
- export function injectStartupMessage(
10
- agentName: string,
11
- pendingCount: number,
12
- inboxItems?: InboxItem[],
13
- ): void {
14
- if (pendingCount === 0) {
15
- const message = `[AgentMesh] Connected and ready. No pending items in inbox.`;
16
- sendKeys(agentName, message);
17
- return;
18
- }
19
-
20
- // Build detailed message with handoff info
21
- let message = `[AgentMesh] Welcome back! You have ${pendingCount} pending handoff${pendingCount === 1 ? "" : "s"}.`;
22
-
23
- if (inboxItems && inboxItems.length > 0) {
24
- message += "\n\n--- PENDING HANDOFFS ---";
25
- for (const item of inboxItems.slice(0, 3)) {
26
- // Show up to 3
27
- const fromName = item.from_agent?.display_name || item.from_agent_id || "Unknown";
28
- message += `\n\nHandoff ID: ${item.id}`;
29
- message += `\nFrom: ${fromName}`;
30
- message += `\nScope: ${item.scope || "Not specified"}`;
31
- message += `\nReason: ${item.reason || "Not specified"}`;
32
- }
33
- if (inboxItems.length > 3) {
34
- message += `\n\n... and ${inboxItems.length - 3} more.`;
35
- }
36
- message += "\n\n--- END HANDOFFS ---";
37
- message += "\n\nReview the handoff(s) above and begin work on the assigned task.";
38
- } else {
39
- message += "\nUse agentmesh_check_inbox to see them.";
40
- }
41
-
42
- sendKeys(agentName, message);
43
- }
44
-
45
- export function injectHandoffReceived(agentName: string, event: WebSocketEvent): void {
46
- const fromName =
47
- (event.from_agent as { display_name?: string })?.display_name ||
48
- (event.from_agent_id as string) ||
49
- "Unknown";
50
- const scope = (event.scope as string) || "No scope provided";
51
- const reason = (event.reason as string) || "No reason provided";
52
- const handoffId = (event.handoff_id as string) || (event.id as string) || "unknown";
53
-
54
- const message = `[AgentMesh] New handoff from ${fromName}
55
-
56
- Scope: ${scope}
57
- Reason: ${reason}
58
- Handoff ID: ${handoffId}
59
-
60
- Accept this handoff and begin work.`;
61
-
62
- sendKeys(agentName, message);
63
- }
64
-
65
- export function injectNudge(agentName: string, event: WebSocketEvent): void {
66
- const fromName =
67
- (event.from as { name?: string })?.name || (event.from_name as string) || "Someone";
68
- const message = (event.message as string) || "Check your inbox";
69
-
70
- const formatted = `[AgentMesh] Nudge from ${fromName}:
71
- ${message}`;
72
-
73
- sendKeys(agentName, formatted);
74
- }
75
-
76
- export function injectBlockerResolved(agentName: string, event: WebSocketEvent): void {
77
- const description = (event.description as string) || "A blocker has been resolved";
78
- const resolvedBy =
79
- (event.resolved_by as { display_name?: string })?.display_name ||
80
- (event.resolved_by_name as string) ||
81
- "Another agent";
82
-
83
- const message = `[AgentMesh] Blocker resolved!
84
-
85
- Blocker: ${description}
86
- Resolved by: ${resolvedBy}
87
-
88
- You can now proceed with your work.`;
89
-
90
- sendKeys(agentName, message);
91
- }
92
-
93
- export interface EventContext {
94
- hubUrl?: string;
95
- token?: string;
96
- }
97
-
98
- interface SlackFile {
99
- name: string;
100
- type: string;
101
- url: string;
102
- permalink: string;
103
- base64?: string;
104
- mediaType?: string;
105
- }
106
-
107
- export function injectSlackMessage(
108
- agentName: string,
109
- event: WebSocketEvent,
110
- _context?: EventContext,
111
- ): void {
112
- const user = (event.user as string) || "unknown";
113
- const channel = (event.channel as string) || "unknown";
114
- const text = (event.text as string) || "";
115
- const files = (event.files as SlackFile[] | undefined) || [];
116
-
117
- // Build message with file info
118
- let message = `[Slack from ${user} in ${channel}] ${text}`;
119
-
120
- if (files.length > 0) {
121
- const savedImages: string[] = [];
122
- const otherFiles: string[] = [];
123
-
124
- // Save images with base64 data to disk
125
- const attachmentsDir = join(process.env.HOME || "/tmp", ".agentmesh", "attachments");
126
- if (!existsSync(attachmentsDir)) {
127
- mkdirSync(attachmentsDir, { recursive: true });
128
- }
129
-
130
- for (const f of files) {
131
- console.log(
132
- `[Injector] File: ${f.name}, type: ${f.type}, mediaType: ${f.mediaType}, has base64: ${!!f.base64}`,
133
- );
134
- if (f.base64 && (f.mediaType?.startsWith("image/") || f.type?.startsWith("image/"))) {
135
- // Save image to disk
136
- const timestamp = Date.now();
137
- const safeName = f.name.replace(/[^a-zA-Z0-9._-]/g, "_");
138
- const filePath = join(attachmentsDir, `${timestamp}-${safeName}`);
139
-
140
- try {
141
- writeFileSync(filePath, Buffer.from(f.base64, "base64"));
142
- savedImages.push(filePath);
143
- console.log(`[Injector] Saved image to: ${filePath}`);
144
- } catch (err) {
145
- console.error(`[Injector] Failed to save image:`, err);
146
- otherFiles.push(`${f.name} (failed to save)`);
147
- }
148
- } else {
149
- // Just reference the URL for non-image files
150
- otherFiles.push(`${f.name}: ${f.permalink}`);
151
- }
152
- }
153
-
154
- // Add instruction to view images - OpenCode needs explicit instruction
155
- if (savedImages.length > 0) {
156
- message += `\n\nUser attached ${savedImages.length} image(s). Use the Read tool to view them:`;
157
- for (const img of savedImages) {
158
- message += `\n- ${img}`;
159
- }
160
- }
161
-
162
- if (otherFiles.length > 0) {
163
- message += ` [Other files: ${otherFiles.join(" | ")}]`;
164
- }
165
- }
166
-
167
- console.log(`[Injector] Sending to tmux: ${message}`);
168
- const result = sendKeys(agentName, message);
169
- console.log(`[Injector] sendKeys result: ${result}`);
170
- }
171
-
172
- export function injectInboxItems(agentName: string, items: InboxItem[]): void {
173
- if (items.length === 0) {
174
- sendKeys(agentName, "[AgentMesh] Your inbox is empty.");
175
- return;
176
- }
177
-
178
- let message = `[AgentMesh] You have ${items.length} pending item${items.length === 1 ? "" : "s"}:\n\n`;
179
-
180
- for (const item of items) {
181
- const fromName = item.from_agent?.display_name || item.from_agent_id;
182
- message += `- From: ${fromName}\n`;
183
- message += ` Scope: ${item.scope}\n`;
184
- message += ` ID: ${item.id}\n\n`;
185
- }
186
-
187
- message += "Use agentmesh_accept_handoff with the ID to start working.";
188
-
189
- sendKeys(agentName, message);
190
- }
191
-
192
- export function handleWebSocketEvent(
193
- agentName: string,
194
- event: WebSocketEvent,
195
- context?: EventContext,
196
- ): void {
197
- switch (event.type) {
198
- case "handoff_received":
199
- case "handoff.received":
200
- injectHandoffReceived(agentName, event);
201
- break;
202
-
203
- case "nudge":
204
- case "agent.nudge":
205
- injectNudge(agentName, event);
206
- break;
207
-
208
- case "blocker_resolved":
209
- case "blocker.resolved":
210
- injectBlockerResolved(agentName, event);
211
- break;
212
-
213
- case "slack.message":
214
- console.log(`[Injector] Handling slack.message for ${agentName}`);
215
- console.log(`[Injector] Event keys: ${Object.keys(event).join(", ")}`);
216
- if (event.files) {
217
- const files = event.files as Array<{ name: string; base64?: string }>;
218
- console.log(
219
- `[Injector] Files count: ${files.length}, has base64: ${files.some((f) => !!f.base64)}`,
220
- );
221
- }
222
- injectSlackMessage(agentName, event, context);
223
- break;
224
-
225
- default:
226
- // Unknown event type, ignore
227
- break;
228
- }
229
- }
230
-
231
- /**
232
- * Injects restored context from a previous session
233
- */
234
- export function injectRestoredContext(agentName: string, context: AgentContext): void {
235
- const parts: string[] = ["[AgentMesh] Restored context from previous session:"];
236
-
237
- // Working state
238
- if (context.workingState.workdir) {
239
- parts.push(`Working directory: ${context.workingState.workdir}`);
240
- }
241
- if (context.workingState.gitBranch) {
242
- parts.push(`Git branch: ${context.workingState.gitBranch}`);
243
- }
244
-
245
- // Tasks
246
- const activeTasks = context.tasks.tasks.filter(
247
- (t) => t.status === "in_progress" || t.status === "pending",
248
- );
249
- if (activeTasks.length > 0) {
250
- parts.push("");
251
- parts.push("Active tasks:");
252
- for (const task of activeTasks.slice(0, 5)) {
253
- const statusIcon = task.status === "in_progress" ? ">" : "-";
254
- parts.push(` ${statusIcon} ${task.content}`);
255
- }
256
- if (activeTasks.length > 5) {
257
- parts.push(` ... and ${activeTasks.length - 5} more`);
258
- }
259
- }
260
-
261
- // Current goal
262
- if (context.tasks.currentGoal) {
263
- parts.push("");
264
- parts.push(`Goal: ${context.tasks.currentGoal}`);
265
- }
266
-
267
- // Accomplishments
268
- if (context.conversation.accomplishments.length > 0) {
269
- parts.push("");
270
- parts.push("Recent accomplishments:");
271
- for (const acc of context.conversation.accomplishments.slice(0, 3)) {
272
- parts.push(` - ${acc}`);
273
- }
274
- }
275
-
276
- const message = parts.join("\n");
277
- sendKeys(agentName, message);
278
- }
279
-
280
- /**
281
- * Injects context received from a handoff
282
- */
283
- export function injectHandoffContext(agentName: string, contextString: string): void {
284
- const context = parseHandoffContext(contextString);
285
- if (!context) {
286
- return;
287
- }
288
-
289
- const summary = formatHandoffContextSummary(context);
290
- const message = `[AgentMesh] Context from previous agent:\n\n${summary}`;
291
- sendKeys(agentName, message);
292
- }
@@ -1,159 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
-
3
- export interface RegisterOptions {
4
- url: string;
5
- apiKey: string;
6
- workspace: string;
7
- agentId?: string;
8
- agentName: string;
9
- model: string;
10
- capabilities?: string[];
11
- restoreContext?: boolean;
12
- }
13
-
14
- export type ServerContext = Record<string, Record<string, unknown>>;
15
-
16
- export interface RegisterResult {
17
- agentId: string;
18
- token: string;
19
- status?: "registered" | "re-registered";
20
- context?: ServerContext;
21
- assignments?: ProjectAssignment[];
22
- }
23
-
24
- export interface InboxItem {
25
- id: string;
26
- from_agent_id: string;
27
- from_agent?: {
28
- display_name?: string;
29
- };
30
- scope: string;
31
- reason: string;
32
- status: string;
33
- created_at: string;
34
- }
35
-
36
- export async function registerAgent(options: RegisterOptions): Promise<RegisterResult> {
37
- const agentId = options.agentId || randomUUID();
38
-
39
- const response = await fetch(`${options.url}/api/v1/agents/register`, {
40
- method: "POST",
41
- headers: {
42
- "x-agentmesh-secret": options.apiKey,
43
- "Content-Type": "application/json",
44
- },
45
- body: JSON.stringify({
46
- agent_id: agentId,
47
- display_name: options.agentName,
48
- model: options.model,
49
- capabilities: options.capabilities || ["coding", "review", "debugging"],
50
- workspace: options.workspace,
51
- restore_context: options.restoreContext ?? true,
52
- }),
53
- });
54
-
55
- if (!response.ok) {
56
- const error = await response.text();
57
- throw new Error(`Failed to register agent: ${error}`);
58
- }
59
-
60
- const data = await response.json();
61
- const token = data.data?.token || data.token;
62
-
63
- if (!token) {
64
- throw new Error("No token in registration response");
65
- }
66
-
67
- return {
68
- agentId,
69
- token,
70
- status: data.status,
71
- context: data.context,
72
- assignments: data.assignments,
73
- };
74
- }
75
-
76
- export async function checkInbox(
77
- url: string,
78
- workspace: string,
79
- token: string,
80
- ): Promise<InboxItem[]> {
81
- const response = await fetch(`${url}/api/v1/workspaces/${workspace}/inbox`, {
82
- headers: {
83
- Authorization: `Bearer ${token}`,
84
- },
85
- });
86
-
87
- if (!response.ok) {
88
- throw new Error(`Failed to check inbox: ${response.status}`);
89
- }
90
-
91
- const data = await response.json();
92
- return (data.data || []).filter((item: InboxItem) => item.status === "pending");
93
- }
94
-
95
- export async function sendNudge(
96
- url: string,
97
- agentId: string,
98
- message: string,
99
- token: string,
100
- ): Promise<boolean> {
101
- const response = await fetch(`${url}/api/v1/agents/${agentId}/nudge`, {
102
- method: "POST",
103
- headers: {
104
- Authorization: `Bearer ${token}`,
105
- "Content-Type": "application/json",
106
- },
107
- body: JSON.stringify({ message }),
108
- });
109
-
110
- return response.ok;
111
- }
112
-
113
- // ============================================================================
114
- // Agent Self-Discovery
115
- // ============================================================================
116
-
117
- export interface ProjectAssignment {
118
- assignment_id: string;
119
- role: string;
120
- status: string;
121
- notes: string | null;
122
- priority: number;
123
- created_at: string;
124
- project: {
125
- project_id: string;
126
- name: string;
127
- code: string;
128
- description: string | null;
129
- workdir: string | null;
130
- };
131
- repo: {
132
- repo_id: string;
133
- provider: string;
134
- full_name: string;
135
- url: string;
136
- default_branch: string;
137
- } | null;
138
- }
139
-
140
- /**
141
- * Fetch the agent's project assignments from HQ
142
- */
143
- export async function fetchAssignments(url: string, token: string): Promise<ProjectAssignment[]> {
144
- const response = await fetch(`${url}/api/v1/agents/me/assignments`, {
145
- headers: {
146
- Authorization: `Bearer ${token}`,
147
- },
148
- });
149
-
150
- if (!response.ok) {
151
- if (response.status === 404) {
152
- return []; // No assignments
153
- }
154
- throw new Error(`Failed to fetch assignments: ${response.status}`);
155
- }
156
-
157
- const data = (await response.json()) as { data: ProjectAssignment[] };
158
- return data.data || [];
159
- }