@agentmeshhq/agent 0.1.9 → 0.1.10

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.
@@ -1,8 +1,11 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
1
5
  import {
2
6
  addAgentToState,
3
7
  getAgentState,
4
8
  loadConfig,
5
- loadState,
6
9
  removeAgentFromState,
7
10
  updateAgentInState,
8
11
  } from "../config/loader.js";
@@ -10,8 +13,13 @@ import type { AgentConfig, Config } from "../config/schema.js";
10
13
  import { loadContext, loadOrCreateContext, saveContext } from "../context/index.js";
11
14
  import { Heartbeat } from "./heartbeat.js";
12
15
  import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
13
- import { checkInbox, registerAgent } from "./registry.js";
14
- import { buildRunnerConfig, getRunnerDisplayName, type RunnerConfig } from "./runner.js";
16
+ import { checkInbox, fetchAssignments, registerAgent } from "./registry.js";
17
+ import {
18
+ buildRunnerConfig,
19
+ detectRunner,
20
+ getRunnerDisplayName,
21
+ type RunnerConfig,
22
+ } from "./runner.js";
15
23
  import {
16
24
  captureSessionContext,
17
25
  createSession,
@@ -30,6 +38,8 @@ export interface DaemonOptions {
30
38
  daemonize?: boolean;
31
39
  /** Whether to restore context from previous session (default: true) */
32
40
  restoreContext?: boolean;
41
+ /** Auto-clone repository for project assignments */
42
+ autoSetup?: boolean;
33
43
  }
34
44
 
35
45
  export class AgentDaemon {
@@ -43,6 +53,7 @@ export class AgentDaemon {
43
53
  private agentId: string | null = null;
44
54
  private isRunning = false;
45
55
  private shouldRestoreContext: boolean;
56
+ private autoSetup: boolean;
46
57
 
47
58
  constructor(options: DaemonOptions) {
48
59
  const config = loadConfig();
@@ -53,6 +64,7 @@ export class AgentDaemon {
53
64
  this.config = config;
54
65
  this.agentName = options.name;
55
66
  this.shouldRestoreContext = options.restoreContext !== false;
67
+ this.autoSetup = options.autoSetup === true;
56
68
 
57
69
  // Find or create agent config
58
70
  let agentConfig = config.agents.find((a) => a.name === options.name);
@@ -84,46 +96,6 @@ export class AgentDaemon {
84
96
  const runnerName = getRunnerDisplayName(this.runnerConfig.type);
85
97
  console.log(`Runner: ${runnerName}`);
86
98
  console.log(`Effective model: ${this.runnerConfig.model}`);
87
-
88
- // Check workdir conflicts - prevent multiple agents from using same directory
89
- this.checkWorkdirConflict(agentConfig.workdir);
90
- }
91
-
92
- /**
93
- * Checks if another agent is already using the specified workdir
94
- */
95
- private checkWorkdirConflict(workdir?: string): void {
96
- if (!workdir) return;
97
-
98
- const state = loadState();
99
- const conflictingAgent = state.agents.find((a) => {
100
- // Skip self
101
- if (a.name === this.agentName) return false;
102
-
103
- // Check if agent is actually running
104
- if (!a.pid) return false;
105
- try {
106
- process.kill(a.pid, 0); // Check if process exists
107
- } catch {
108
- return false; // Process not running
109
- }
110
-
111
- // Check if session exists and has same workdir
112
- // We need to check the config for this agent's workdir
113
- const otherAgentConfig = this.config.agents.find((c) => c.name === a.name);
114
- if (otherAgentConfig?.workdir === workdir) {
115
- return true;
116
- }
117
-
118
- return false;
119
- });
120
-
121
- if (conflictingAgent) {
122
- throw new Error(
123
- `Workdir conflict: Agent "${conflictingAgent.name}" is already using "${workdir}".\n` +
124
- `Use a different --workdir or stop the other agent first.`,
125
- );
126
- }
127
99
  }
128
100
 
129
101
  async start(): Promise<void> {
@@ -134,6 +106,27 @@ export class AgentDaemon {
134
106
 
135
107
  console.log(`Starting agent: ${this.agentName}`);
136
108
 
109
+ // Register with hub first (needed for assignment check)
110
+ console.log("Registering with AgentMesh hub...");
111
+ const existingState = getAgentState(this.agentName);
112
+
113
+ const registration = await registerAgent({
114
+ url: this.config.hubUrl,
115
+ apiKey: this.config.apiKey,
116
+ workspace: this.config.workspace,
117
+ agentId: existingState?.agentId || this.agentConfig.agentId,
118
+ agentName: this.agentName,
119
+ model: this.agentConfig.model || this.config.defaults.model,
120
+ });
121
+
122
+ this.agentId = registration.agentId;
123
+ this.token = registration.token;
124
+
125
+ console.log(`Registered as: ${this.agentId}`);
126
+
127
+ // Check assignments and auto-setup workdir if needed (before creating tmux session)
128
+ await this.checkAssignments();
129
+
137
130
  // Check if session already exists
138
131
  const sessionName = getSessionName(this.agentName);
139
132
  const sessionAlreadyExists = sessionExists(sessionName);
@@ -141,11 +134,13 @@ export class AgentDaemon {
141
134
  // Create tmux session if it doesn't exist
142
135
  if (!sessionAlreadyExists) {
143
136
  console.log(`Creating tmux session: ${sessionName}`);
137
+
138
+ // Include runner env vars (e.g., OPENCODE_MODEL) at session creation
144
139
  const created = createSession(
145
140
  this.agentName,
146
141
  this.agentConfig.command,
147
142
  this.agentConfig.workdir,
148
- this.runnerConfig.env, // Pass runner env vars (e.g., OPENCODE_MODEL)
143
+ this.runnerConfig.env, // Apply model env at process start
149
144
  );
150
145
 
151
146
  if (!created) {
@@ -154,29 +149,9 @@ export class AgentDaemon {
154
149
  } else {
155
150
  console.log(`Reconnecting to existing session: ${sessionName}`);
156
151
  // Update environment for existing session
157
- if (Object.keys(this.runnerConfig.env).length > 0) {
158
- updateSessionEnvironment(this.agentName, this.runnerConfig.env);
159
- }
152
+ updateSessionEnvironment(this.agentName, this.runnerConfig.env);
160
153
  }
161
154
 
162
- // Register with hub
163
- console.log("Registering with AgentMesh hub...");
164
- const existingState = getAgentState(this.agentName);
165
-
166
- const registration = await registerAgent({
167
- url: this.config.hubUrl,
168
- apiKey: this.config.apiKey,
169
- workspace: this.config.workspace,
170
- agentId: existingState?.agentId || this.agentConfig.agentId,
171
- agentName: this.agentName,
172
- model: this.agentConfig.model || this.config.defaults.model,
173
- });
174
-
175
- this.agentId = registration.agentId;
176
- this.token = registration.token;
177
-
178
- console.log(`Registered as: ${this.agentId}`);
179
-
180
155
  // Inject environment variables into tmux session
181
156
  console.log("Injecting environment variables...");
182
157
  updateSessionEnvironment(this.agentName, {
@@ -184,7 +159,7 @@ export class AgentDaemon {
184
159
  AGENTMESH_AGENT_ID: this.agentId,
185
160
  });
186
161
 
187
- // Save state
162
+ // Save state including runtime model info
188
163
  addAgentToState({
189
164
  name: this.agentName,
190
165
  agentId: this.agentId,
@@ -376,4 +351,170 @@ Nudge agent:
376
351
  console.error("Failed to save agent context:", error);
377
352
  }
378
353
  }
354
+
355
+ /**
356
+ * Fetches assignments from HQ and validates workdir setup
357
+ * Uses project.workdir from HQ as source of truth, falls back to helpful instructions
358
+ */
359
+ private async checkAssignments(): Promise<void> {
360
+ if (!this.token) return;
361
+
362
+ try {
363
+ console.log("Fetching project assignments from HQ...");
364
+ const assignments = await fetchAssignments(this.config.hubUrl, this.token);
365
+
366
+ if (assignments.length === 0) {
367
+ console.log("No project assignments found.");
368
+ return;
369
+ }
370
+
371
+ console.log(`Found ${assignments.length} assignment(s):`);
372
+ for (const assignment of assignments) {
373
+ const repoInfo = assignment.repo ? ` -> ${assignment.repo.full_name}` : "";
374
+ const workdirInfo = assignment.project.workdir ? ` [${assignment.project.workdir}]` : "";
375
+ console.log(` - ${assignment.project.name} (${assignment.role})${repoInfo}${workdirInfo}`);
376
+ }
377
+
378
+ // If no CLI workdir specified, try to use project.workdir from HQ
379
+ if (!this.agentConfig.workdir) {
380
+ const assignmentWithWorkdir = assignments.find((a) => a.project.workdir);
381
+ if (assignmentWithWorkdir?.project.workdir) {
382
+ console.log(
383
+ `Using workdir from project settings: ${assignmentWithWorkdir.project.workdir}`,
384
+ );
385
+ this.agentConfig.workdir = assignmentWithWorkdir.project.workdir;
386
+ return;
387
+ }
388
+
389
+ // No project.workdir set, check if we have a repo assignment
390
+ const repoAssignment = assignments.find((a) => a.repo !== null);
391
+ if (repoAssignment) {
392
+ const repo = repoAssignment.repo!;
393
+ const expandedPath = path.join(
394
+ os.homedir(),
395
+ ".agentmesh",
396
+ "workspaces",
397
+ this.config.workspace,
398
+ repoAssignment.project.code.toLowerCase(),
399
+ this.agentName,
400
+ );
401
+ const suggestedPath = `~/.agentmesh/workspaces/${this.config.workspace}/${repoAssignment.project.code.toLowerCase()}/${this.agentName}`;
402
+
403
+ // If --auto-setup is enabled, automatically clone the repo
404
+ if (this.autoSetup) {
405
+ this.agentConfig.workdir = this.setupWorkspace(
406
+ expandedPath,
407
+ repo.url,
408
+ repo.default_branch,
409
+ repoAssignment.project.name,
410
+ );
411
+ return;
412
+ }
413
+
414
+ console.error(`
415
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
416
+ ⚠️ WORKDIR REQUIRED
417
+
418
+ You have a project assignment with a repository, but no workdir is configured.
419
+
420
+ Project: ${repoAssignment.project.name}
421
+ Repo: ${repo.full_name}
422
+ Branch: ${repo.default_branch}
423
+
424
+ Option 1: Set workdir in project settings (recommended)
425
+ - Go to AgentMesh HQ → Projects → ${repoAssignment.project.name} → Settings
426
+ - Set the workdir field to the local path
427
+
428
+ Option 2: Set up workspace manually and pass --workdir:
429
+
430
+ mkdir -p ${suggestedPath}
431
+ git clone ${repo.url} ${suggestedPath}
432
+ cd ${suggestedPath} && git checkout ${repo.default_branch}
433
+
434
+ Then start the agent with:
435
+
436
+ agentmesh start -n ${this.agentName} --workdir ${suggestedPath}
437
+
438
+ Option 3: Use --auto-setup to automatically clone the repository:
439
+
440
+ agentmesh start -n ${this.agentName} --auto-setup
441
+
442
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
443
+ `);
444
+ // No session to clean up - we haven't created it yet
445
+ process.exit(1);
446
+ }
447
+ }
448
+ } catch (error) {
449
+ // Non-fatal: log and continue (HQ might not have assignments feature yet)
450
+ console.log("Could not fetch assignments:", (error as Error).message);
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Sets up workspace by cloning repository or using existing clone
456
+ * Returns the absolute path to the workspace
457
+ */
458
+ private setupWorkspace(
459
+ workspacePath: string,
460
+ repoUrl: string,
461
+ defaultBranch: string,
462
+ projectName: string,
463
+ ): string {
464
+ console.log(
465
+ `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
466
+ );
467
+ console.log(`🔧 AUTO-SETUP: Setting up workspace for ${projectName}`);
468
+ console.log(
469
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`,
470
+ );
471
+
472
+ // Check if directory already exists and is a git repo
473
+ const gitDir = path.join(workspacePath, ".git");
474
+ if (fs.existsSync(gitDir)) {
475
+ console.log(`✓ Workspace already exists: ${workspacePath}`);
476
+ console.log(` Updating from remote...`);
477
+
478
+ try {
479
+ // Fetch and checkout the branch
480
+ execSync(`git fetch origin`, { cwd: workspacePath, stdio: "inherit" });
481
+ execSync(`git checkout ${defaultBranch}`, { cwd: workspacePath, stdio: "inherit" });
482
+ execSync(`git pull origin ${defaultBranch}`, { cwd: workspacePath, stdio: "inherit" });
483
+ console.log(`✓ Workspace updated successfully\n`);
484
+ } catch (error) {
485
+ console.warn(`⚠ Could not update workspace: ${(error as Error).message}`);
486
+ console.log(` Continuing with existing state...\n`);
487
+ }
488
+
489
+ return workspacePath;
490
+ }
491
+
492
+ // Create parent directories
493
+ const parentDir = path.dirname(workspacePath);
494
+ if (!fs.existsSync(parentDir)) {
495
+ console.log(`Creating directory: ${parentDir}`);
496
+ fs.mkdirSync(parentDir, { recursive: true });
497
+ }
498
+
499
+ // Clone the repository
500
+ console.log(`Cloning repository...`);
501
+ console.log(` URL: ${repoUrl}`);
502
+ console.log(` Path: ${workspacePath}`);
503
+ console.log(` Branch: ${defaultBranch}\n`);
504
+
505
+ try {
506
+ execSync(`git clone --branch ${defaultBranch} "${repoUrl}" "${workspacePath}"`, {
507
+ stdio: "inherit",
508
+ });
509
+ console.log(`\n✓ Repository cloned successfully`);
510
+ } catch (error) {
511
+ console.error(`\n✗ Failed to clone repository: ${(error as Error).message}`);
512
+ console.error(`\nMake sure you have access to the repository and SSH keys are configured.`);
513
+ // No session to clean up - we haven't created it yet
514
+ process.exit(1);
515
+ }
516
+
517
+ console.log(`✓ Workspace ready: ${workspacePath}\n`);
518
+ return workspacePath;
519
+ }
379
520
  }
@@ -27,9 +27,7 @@ export interface InboxItem {
27
27
  created_at: string;
28
28
  }
29
29
 
30
- export async function registerAgent(
31
- options: RegisterOptions
32
- ): Promise<RegisterResult> {
30
+ export async function registerAgent(options: RegisterOptions): Promise<RegisterResult> {
33
31
  const agentId = options.agentId || randomUUID();
34
32
 
35
33
  const response = await fetch(`${options.url}/api/v1/agents/register`, {
@@ -65,32 +63,27 @@ export async function registerAgent(
65
63
  export async function checkInbox(
66
64
  url: string,
67
65
  workspace: string,
68
- token: string
66
+ token: string,
69
67
  ): Promise<InboxItem[]> {
70
- const response = await fetch(
71
- `${url}/api/v1/workspaces/${workspace}/inbox`,
72
- {
73
- headers: {
74
- Authorization: `Bearer ${token}`,
75
- },
76
- }
77
- );
68
+ const response = await fetch(`${url}/api/v1/workspaces/${workspace}/inbox`, {
69
+ headers: {
70
+ Authorization: `Bearer ${token}`,
71
+ },
72
+ });
78
73
 
79
74
  if (!response.ok) {
80
75
  throw new Error(`Failed to check inbox: ${response.status}`);
81
76
  }
82
77
 
83
78
  const data = await response.json();
84
- return (data.data || []).filter(
85
- (item: InboxItem) => item.status === "pending"
86
- );
79
+ return (data.data || []).filter((item: InboxItem) => item.status === "pending");
87
80
  }
88
81
 
89
82
  export async function sendNudge(
90
83
  url: string,
91
84
  agentId: string,
92
85
  message: string,
93
- token: string
86
+ token: string,
94
87
  ): Promise<boolean> {
95
88
  const response = await fetch(`${url}/api/v1/agents/${agentId}/nudge`, {
96
89
  method: "POST",
@@ -103,3 +96,51 @@ export async function sendNudge(
103
96
 
104
97
  return response.ok;
105
98
  }
99
+
100
+ // ============================================================================
101
+ // Agent Self-Discovery
102
+ // ============================================================================
103
+
104
+ export interface ProjectAssignment {
105
+ assignment_id: string;
106
+ role: string;
107
+ status: string;
108
+ notes: string | null;
109
+ priority: number;
110
+ created_at: string;
111
+ project: {
112
+ project_id: string;
113
+ name: string;
114
+ code: string;
115
+ description: string | null;
116
+ workdir: string | null;
117
+ };
118
+ repo: {
119
+ repo_id: string;
120
+ provider: string;
121
+ full_name: string;
122
+ url: string;
123
+ default_branch: string;
124
+ } | null;
125
+ }
126
+
127
+ /**
128
+ * Fetch the agent's project assignments from HQ
129
+ */
130
+ export async function fetchAssignments(url: string, token: string): Promise<ProjectAssignment[]> {
131
+ const response = await fetch(`${url}/api/v1/agents/me/assignments`, {
132
+ headers: {
133
+ Authorization: `Bearer ${token}`,
134
+ },
135
+ });
136
+
137
+ if (!response.ok) {
138
+ if (response.status === 404) {
139
+ return []; // No assignments
140
+ }
141
+ throw new Error(`Failed to fetch assignments: ${response.status}`);
142
+ }
143
+
144
+ const data = (await response.json()) as { data: ProjectAssignment[] };
145
+ return data.data || [];
146
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
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.