@agentmeshhq/agent 0.1.9 → 0.1.11

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/src/cli/start.ts CHANGED
@@ -13,6 +13,7 @@ export interface StartOptions {
13
13
  model?: string;
14
14
  foreground?: boolean;
15
15
  noContext?: boolean;
16
+ autoSetup?: boolean;
16
17
  }
17
18
 
18
19
  export async function start(options: StartOptions): Promise<void> {
@@ -44,6 +45,7 @@ export async function start(options: StartOptions): Promise<void> {
44
45
  const daemon = new AgentDaemon({
45
46
  ...options,
46
47
  restoreContext: !options.noContext,
48
+ autoSetup: options.autoSetup,
47
49
  });
48
50
  await daemon.start();
49
51
  // Keep process alive
@@ -67,6 +69,7 @@ export async function start(options: StartOptions): Promise<void> {
67
69
  if (options.workdir) args.push("--workdir", options.workdir);
68
70
  if (options.model) args.push("--model", options.model);
69
71
  if (options.noContext) args.push("--no-context");
72
+ if (options.autoSetup) args.push("--auto-setup");
70
73
 
71
74
  // Spawn detached background process
72
75
  const child = spawn("node", [cliPath, ...args], {
package/src/cli/status.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import pc from "picocolors";
2
2
  import { loadConfig, loadState } from "../config/loader.js";
3
- import { getRunnerDisplayName } from "../core/runner.js";
4
3
  import { getSessionName, sessionExists } from "../core/tmux.js";
5
4
 
6
5
  interface HealthResponse {
@@ -61,10 +60,14 @@ export async function status(): Promise<void> {
61
60
  console.log(` Total: ${pc.dim(String(state.agents.length))}`);
62
61
 
63
62
  if (runningAgents.length > 0) {
64
- console.log(` Active:`);
65
- for (const agent of runningAgents) {
66
- const modelInfo = agent.runtimeModel ? ` [${agent.runtimeModel}]` : "";
67
- console.log(` - ${pc.cyan(agent.name)}${pc.dim(modelInfo)}`);
63
+ console.log(` Names: ${pc.dim(runningAgents.map((a) => a.name).join(", "))}`);
64
+
65
+ // Show models for running agents
66
+ const modelsInfo = runningAgents
67
+ .filter((a) => a.runtimeModel)
68
+ .map((a) => `${a.name}:${a.runtimeModel}`);
69
+ if (modelsInfo.length > 0) {
70
+ console.log(` Models: ${pc.dim(modelsInfo.join(", "))}`);
68
71
  }
69
72
  }
70
73
 
package/src/cli/whoami.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import pc from "picocolors";
2
2
  import { loadConfig, loadState } from "../config/loader.js";
3
- import { getRunnerDisplayName, type RunnerType } from "../core/runner.js";
4
- import { decodeToken, getTokenExpiry } from "../utils/jwt.js";
3
+ import type { RunnerType } from "../config/schema.js";
4
+ import { getRunnerDisplayName } from "../core/runner.js";
5
+ import { getTokenExpiry } from "../utils/jwt.js";
5
6
 
6
7
  export async function whoami(agentName?: string): Promise<void> {
7
8
  const config = loadConfig();
@@ -24,20 +25,23 @@ export async function whoami(agentName?: string): Promise<void> {
24
25
  const expiry = getTokenExpiry(envToken);
25
26
  const expiryStr = formatExpiry(expiry);
26
27
 
27
- // Find the agent in state to get model info
28
+ // Try to find agent in state for runtime info
28
29
  const agentState = state.agents.find((a) => a.agentId === envAgentId);
29
30
 
30
31
  console.log(pc.bold("Current Agent"));
31
32
  console.log(` ID: ${pc.cyan(envAgentId)}`);
32
33
  console.log(` Workspace: ${pc.dim(config.workspace)}`);
34
+ console.log(` Token: ${expiryStr}`);
35
+ console.log(` Hub: ${pc.dim(config.hubUrl)}`);
36
+
37
+ // Show runtime model info if available
33
38
  if (agentState?.runtimeModel) {
34
39
  const runnerName = agentState.runnerType
35
- ? getRunnerDisplayName(agentState.runnerType)
40
+ ? getRunnerDisplayName(agentState.runnerType as RunnerType)
36
41
  : "Unknown";
37
- console.log(` Model: ${pc.cyan(agentState.runtimeModel)} (${pc.dim(runnerName)})`);
42
+ console.log(` Model: ${pc.cyan(agentState.runtimeModel)}`);
43
+ console.log(` Runner: ${pc.dim(runnerName)}`);
38
44
  }
39
- console.log(` Token: ${expiryStr}`);
40
- console.log(` Hub: ${pc.dim(config.hubUrl)}`);
41
45
  return;
42
46
  }
43
47
 
@@ -59,11 +63,13 @@ export async function whoami(agentName?: string): Promise<void> {
59
63
  console.log(` ${pc.cyan(agent.name)}`);
60
64
  console.log(` ID: ${pc.dim(agent.agentId)}`);
61
65
  console.log(` Status: ${running ? pc.green("running") : pc.yellow("stopped")}`);
66
+ console.log(` Token: ${expiryStr}`);
62
67
  if (agent.runtimeModel) {
63
- const runnerName = agent.runnerType ? getRunnerDisplayName(agent.runnerType) : "Unknown";
68
+ const runnerName = agent.runnerType
69
+ ? getRunnerDisplayName(agent.runnerType as RunnerType)
70
+ : "Unknown";
64
71
  console.log(` Model: ${pc.dim(agent.runtimeModel)} (${runnerName})`);
65
72
  }
66
- console.log(` Token: ${expiryStr}`);
67
73
  console.log();
68
74
  }
69
75
  return;
@@ -85,14 +91,19 @@ export async function whoami(agentName?: string): Promise<void> {
85
91
  console.log(` ID: ${pc.cyan(agent.agentId)}`);
86
92
  console.log(` Workspace: ${pc.dim(config.workspace)}`);
87
93
  console.log(` Status: ${running ? pc.green("running") : pc.yellow("stopped")}`);
88
- if (agent.runtimeModel) {
89
- const runnerName = agent.runnerType ? getRunnerDisplayName(agent.runnerType) : "Unknown";
90
- console.log(` Model: ${pc.cyan(agent.runtimeModel)} (${pc.dim(runnerName)})`);
91
- }
92
94
  console.log(` Token: ${expiryStr}`);
93
95
  console.log(` Session: ${pc.dim(agent.tmuxSession || "none")}`);
94
96
  console.log(` Started: ${pc.dim(agent.startedAt || "unknown")}`);
95
97
  console.log(` Hub: ${pc.dim(config.hubUrl)}`);
98
+
99
+ // Show runtime model info
100
+ if (agent.runtimeModel) {
101
+ const runnerName = agent.runnerType
102
+ ? getRunnerDisplayName(agent.runnerType as RunnerType)
103
+ : "Unknown";
104
+ console.log(` Model: ${pc.cyan(agent.runtimeModel)}`);
105
+ console.log(` Runner: ${pc.dim(runnerName)}`);
106
+ }
96
107
  }
97
108
 
98
109
  function formatExpiry(expiry: Date | null): string {
@@ -39,6 +39,8 @@ export interface AgentState {
39
39
  tmuxSession: string;
40
40
  startedAt: string;
41
41
  token?: string;
42
+ workdir?: string;
43
+ assignedProject?: string;
42
44
  /** The effective runtime model (resolved from CLI > agent > defaults) */
43
45
  runtimeModel?: string;
44
46
  /** The runner type (opencode, claude, custom) */
@@ -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 {
@@ -42,7 +52,9 @@ export class AgentDaemon {
42
52
  private token: string | null = null;
43
53
  private agentId: string | null = null;
44
54
  private isRunning = false;
55
+ private assignedProject: string | undefined;
45
56
  private shouldRestoreContext: boolean;
57
+ private autoSetup: boolean;
46
58
 
47
59
  constructor(options: DaemonOptions) {
48
60
  const config = loadConfig();
@@ -53,6 +65,7 @@ export class AgentDaemon {
53
65
  this.config = config;
54
66
  this.agentName = options.name;
55
67
  this.shouldRestoreContext = options.restoreContext !== false;
68
+ this.autoSetup = options.autoSetup === true;
56
69
 
57
70
  // Find or create agent config
58
71
  let agentConfig = config.agents.find((a) => a.name === options.name);
@@ -84,46 +97,6 @@ export class AgentDaemon {
84
97
  const runnerName = getRunnerDisplayName(this.runnerConfig.type);
85
98
  console.log(`Runner: ${runnerName}`);
86
99
  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
100
  }
128
101
 
129
102
  async start(): Promise<void> {
@@ -134,6 +107,27 @@ export class AgentDaemon {
134
107
 
135
108
  console.log(`Starting agent: ${this.agentName}`);
136
109
 
110
+ // Register with hub first (needed for assignment check)
111
+ console.log("Registering with AgentMesh hub...");
112
+ const existingState = getAgentState(this.agentName);
113
+
114
+ const registration = await registerAgent({
115
+ url: this.config.hubUrl,
116
+ apiKey: this.config.apiKey,
117
+ workspace: this.config.workspace,
118
+ agentId: existingState?.agentId || this.agentConfig.agentId,
119
+ agentName: this.agentName,
120
+ model: this.agentConfig.model || this.config.defaults.model,
121
+ });
122
+
123
+ this.agentId = registration.agentId;
124
+ this.token = registration.token;
125
+
126
+ console.log(`Registered as: ${this.agentId}`);
127
+
128
+ // Check assignments and auto-setup workdir if needed (before creating tmux session)
129
+ await this.checkAssignments();
130
+
137
131
  // Check if session already exists
138
132
  const sessionName = getSessionName(this.agentName);
139
133
  const sessionAlreadyExists = sessionExists(sessionName);
@@ -141,11 +135,13 @@ export class AgentDaemon {
141
135
  // Create tmux session if it doesn't exist
142
136
  if (!sessionAlreadyExists) {
143
137
  console.log(`Creating tmux session: ${sessionName}`);
138
+
139
+ // Include runner env vars (e.g., OPENCODE_MODEL) at session creation
144
140
  const created = createSession(
145
141
  this.agentName,
146
142
  this.agentConfig.command,
147
143
  this.agentConfig.workdir,
148
- this.runnerConfig.env, // Pass runner env vars (e.g., OPENCODE_MODEL)
144
+ this.runnerConfig.env, // Apply model env at process start
149
145
  );
150
146
 
151
147
  if (!created) {
@@ -154,29 +150,9 @@ export class AgentDaemon {
154
150
  } else {
155
151
  console.log(`Reconnecting to existing session: ${sessionName}`);
156
152
  // Update environment for existing session
157
- if (Object.keys(this.runnerConfig.env).length > 0) {
158
- updateSessionEnvironment(this.agentName, this.runnerConfig.env);
159
- }
153
+ updateSessionEnvironment(this.agentName, this.runnerConfig.env);
160
154
  }
161
155
 
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
156
  // Inject environment variables into tmux session
181
157
  console.log("Injecting environment variables...");
182
158
  updateSessionEnvironment(this.agentName, {
@@ -184,7 +160,7 @@ export class AgentDaemon {
184
160
  AGENTMESH_AGENT_ID: this.agentId,
185
161
  });
186
162
 
187
- // Save state
163
+ // Save state including runtime model info
188
164
  addAgentToState({
189
165
  name: this.agentName,
190
166
  agentId: this.agentId,
@@ -192,6 +168,8 @@ export class AgentDaemon {
192
168
  tmuxSession: sessionName,
193
169
  startedAt: new Date().toISOString(),
194
170
  token: this.token,
171
+ workdir: this.agentConfig.workdir,
172
+ assignedProject: this.assignedProject,
195
173
  runtimeModel: this.runnerConfig.model,
196
174
  runnerType: this.runnerConfig.type,
197
175
  });
@@ -376,4 +354,172 @@ Nudge agent:
376
354
  console.error("Failed to save agent context:", error);
377
355
  }
378
356
  }
357
+
358
+ /**
359
+ * Fetches assignments from HQ and validates workdir setup
360
+ * Uses project.workdir from HQ as source of truth, falls back to helpful instructions
361
+ */
362
+ private async checkAssignments(): Promise<void> {
363
+ if (!this.token) return;
364
+
365
+ try {
366
+ console.log("Fetching project assignments from HQ...");
367
+ const assignments = await fetchAssignments(this.config.hubUrl, this.token);
368
+
369
+ if (assignments.length === 0) {
370
+ console.log("No project assignments found.");
371
+ return;
372
+ }
373
+
374
+ console.log(`Found ${assignments.length} assignment(s):`);
375
+ for (const assignment of assignments) {
376
+ const repoInfo = assignment.repo ? ` -> ${assignment.repo.full_name}` : "";
377
+ const workdirInfo = assignment.project.workdir ? ` [${assignment.project.workdir}]` : "";
378
+ console.log(` - ${assignment.project.name} (${assignment.role})${repoInfo}${workdirInfo}`);
379
+ }
380
+
381
+ this.assignedProject = assignments[0]?.project.name;
382
+
383
+ // If no CLI workdir specified, try to use project.workdir from HQ
384
+ if (!this.agentConfig.workdir) {
385
+ const assignmentWithWorkdir = assignments.find((a) => a.project.workdir);
386
+ if (assignmentWithWorkdir?.project.workdir) {
387
+ console.log(
388
+ `Using workdir from project settings: ${assignmentWithWorkdir.project.workdir}`,
389
+ );
390
+ this.agentConfig.workdir = assignmentWithWorkdir.project.workdir;
391
+ return;
392
+ }
393
+
394
+ // No project.workdir set, check if we have a repo assignment
395
+ const repoAssignment = assignments.find((a) => a.repo !== null);
396
+ if (repoAssignment) {
397
+ const repo = repoAssignment.repo!;
398
+ const expandedPath = path.join(
399
+ os.homedir(),
400
+ ".agentmesh",
401
+ "workspaces",
402
+ this.config.workspace,
403
+ repoAssignment.project.code.toLowerCase(),
404
+ this.agentName,
405
+ );
406
+ const suggestedPath = `~/.agentmesh/workspaces/${this.config.workspace}/${repoAssignment.project.code.toLowerCase()}/${this.agentName}`;
407
+
408
+ // If --auto-setup is enabled, automatically clone the repo
409
+ if (this.autoSetup) {
410
+ this.agentConfig.workdir = this.setupWorkspace(
411
+ expandedPath,
412
+ repo.url,
413
+ repo.default_branch,
414
+ repoAssignment.project.name,
415
+ );
416
+ return;
417
+ }
418
+
419
+ console.error(`
420
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
421
+ ⚠️ WORKDIR REQUIRED
422
+
423
+ You have a project assignment with a repository, but no workdir is configured.
424
+
425
+ Project: ${repoAssignment.project.name}
426
+ Repo: ${repo.full_name}
427
+ Branch: ${repo.default_branch}
428
+
429
+ Option 1: Set workdir in project settings (recommended)
430
+ - Go to AgentMesh HQ → Projects → ${repoAssignment.project.name} → Settings
431
+ - Set the workdir field to the local path
432
+
433
+ Option 2: Set up workspace manually and pass --workdir:
434
+
435
+ mkdir -p ${suggestedPath}
436
+ git clone ${repo.url} ${suggestedPath}
437
+ cd ${suggestedPath} && git checkout ${repo.default_branch}
438
+
439
+ Then start the agent with:
440
+
441
+ agentmesh start -n ${this.agentName} --workdir ${suggestedPath}
442
+
443
+ Option 3: Use --auto-setup to automatically clone the repository:
444
+
445
+ agentmesh start -n ${this.agentName} --auto-setup
446
+
447
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
448
+ `);
449
+ // No session to clean up - we haven't created it yet
450
+ process.exit(1);
451
+ }
452
+ }
453
+ } catch (error) {
454
+ // Non-fatal: log and continue (HQ might not have assignments feature yet)
455
+ console.log("Could not fetch assignments:", (error as Error).message);
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Sets up workspace by cloning repository or using existing clone
461
+ * Returns the absolute path to the workspace
462
+ */
463
+ private setupWorkspace(
464
+ workspacePath: string,
465
+ repoUrl: string,
466
+ defaultBranch: string,
467
+ projectName: string,
468
+ ): string {
469
+ console.log(
470
+ `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
471
+ );
472
+ console.log(`🔧 AUTO-SETUP: Setting up workspace for ${projectName}`);
473
+ console.log(
474
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`,
475
+ );
476
+
477
+ // Check if directory already exists and is a git repo
478
+ const gitDir = path.join(workspacePath, ".git");
479
+ if (fs.existsSync(gitDir)) {
480
+ console.log(`✓ Workspace already exists: ${workspacePath}`);
481
+ console.log(` Updating from remote...`);
482
+
483
+ try {
484
+ // Fetch and checkout the branch
485
+ execSync(`git fetch origin`, { cwd: workspacePath, stdio: "inherit" });
486
+ execSync(`git checkout ${defaultBranch}`, { cwd: workspacePath, stdio: "inherit" });
487
+ execSync(`git pull origin ${defaultBranch}`, { cwd: workspacePath, stdio: "inherit" });
488
+ console.log(`✓ Workspace updated successfully\n`);
489
+ } catch (error) {
490
+ console.warn(`⚠ Could not update workspace: ${(error as Error).message}`);
491
+ console.log(` Continuing with existing state...\n`);
492
+ }
493
+
494
+ return workspacePath;
495
+ }
496
+
497
+ // Create parent directories
498
+ const parentDir = path.dirname(workspacePath);
499
+ if (!fs.existsSync(parentDir)) {
500
+ console.log(`Creating directory: ${parentDir}`);
501
+ fs.mkdirSync(parentDir, { recursive: true });
502
+ }
503
+
504
+ // Clone the repository
505
+ console.log(`Cloning repository...`);
506
+ console.log(` URL: ${repoUrl}`);
507
+ console.log(` Path: ${workspacePath}`);
508
+ console.log(` Branch: ${defaultBranch}\n`);
509
+
510
+ try {
511
+ execSync(`git clone --branch ${defaultBranch} "${repoUrl}" "${workspacePath}"`, {
512
+ stdio: "inherit",
513
+ });
514
+ console.log(`\n✓ Repository cloned successfully`);
515
+ } catch (error) {
516
+ console.error(`\n✗ Failed to clone repository: ${(error as Error).message}`);
517
+ console.error(`\nMake sure you have access to the repository and SSH keys are configured.`);
518
+ // No session to clean up - we haven't created it yet
519
+ process.exit(1);
520
+ }
521
+
522
+ console.log(`✓ Workspace ready: ${workspacePath}\n`);
523
+ return workspacePath;
524
+ }
379
525
  }
@@ -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/src/core/tmux.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ChildProcess, execSync, spawn } from "node:child_process";
1
+ import { type ChildProcess, execFileSync, execSync, spawn } from "node:child_process";
2
2
 
3
3
  const SESSION_PREFIX = "agentmesh-";
4
4
 
@@ -64,7 +64,7 @@ export function createSession(
64
64
 
65
65
  args.push(fullCommand);
66
66
 
67
- execSync(`tmux ${args.join(" ")}`);
67
+ execFileSync("tmux", args);
68
68
 
69
69
  // Also set session environment for any subsequent processes/refreshes
70
70
  if (env) {
@@ -82,7 +82,7 @@ export function setSessionEnvironment(sessionName: string, env: SessionEnv): boo
82
82
  try {
83
83
  for (const [key, value] of Object.entries(env)) {
84
84
  if (value !== undefined && value !== "") {
85
- execSync(`tmux set-environment -t "${sessionName}" ${key} "${value}"`);
85
+ execFileSync("tmux", ["set-environment", "-t", sessionName, key, value]);
86
86
  }
87
87
  }
88
88
  return true;
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.