@agentmeshhq/agent 0.1.8 → 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.
- package/dist/__tests__/runner.test.d.ts +1 -0
- package/dist/__tests__/runner.test.js +87 -0
- package/dist/__tests__/runner.test.js.map +1 -0
- package/dist/cli/index.js +5 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/restart.d.ts +3 -1
- package/dist/cli/restart.js +11 -3
- package/dist/cli/restart.js.map +1 -1
- package/dist/cli/start.d.ts +1 -0
- package/dist/cli/start.js +3 -0
- package/dist/cli/start.js.map +1 -1
- package/dist/cli/status.js +7 -0
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/whoami.js +25 -0
- package/dist/cli/whoami.js.map +1 -1
- package/dist/config/schema.d.ts +5 -0
- package/dist/core/daemon.d.ts +14 -0
- package/dist/core/daemon.js +173 -17
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/registry.d.ts +26 -0
- package/dist/core/registry.js +18 -0
- package/dist/core/registry.js.map +1 -1
- package/dist/core/runner.d.ts +49 -0
- package/dist/core/runner.js +148 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/tmux.d.ts +3 -0
- package/dist/core/tmux.js +23 -7
- package/dist/core/tmux.js.map +1 -1
- package/package.json +11 -11
- package/src/__tests__/runner.test.ts +105 -0
- package/src/cli/index.ts +5 -3
- package/src/cli/restart.ts +12 -3
- package/src/cli/start.ts +3 -0
- package/src/cli/status.ts +8 -0
- package/src/cli/whoami.ts +30 -1
- package/src/config/schema.ts +6 -0
- package/src/core/daemon.ts +223 -20
- package/src/core/registry.ts +57 -16
- package/src/core/runner.ts +200 -0
- package/src/core/tmux.ts +28 -9
- package/LICENSE +0 -21
package/src/core/daemon.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
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,
|
|
@@ -9,7 +13,13 @@ import type { AgentConfig, Config } from "../config/schema.js";
|
|
|
9
13
|
import { loadContext, loadOrCreateContext, saveContext } from "../context/index.js";
|
|
10
14
|
import { Heartbeat } from "./heartbeat.js";
|
|
11
15
|
import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
|
|
12
|
-
import { checkInbox, registerAgent } from "./registry.js";
|
|
16
|
+
import { checkInbox, fetchAssignments, registerAgent } from "./registry.js";
|
|
17
|
+
import {
|
|
18
|
+
buildRunnerConfig,
|
|
19
|
+
detectRunner,
|
|
20
|
+
getRunnerDisplayName,
|
|
21
|
+
type RunnerConfig,
|
|
22
|
+
} from "./runner.js";
|
|
13
23
|
import {
|
|
14
24
|
captureSessionContext,
|
|
15
25
|
createSession,
|
|
@@ -28,18 +38,22 @@ export interface DaemonOptions {
|
|
|
28
38
|
daemonize?: boolean;
|
|
29
39
|
/** Whether to restore context from previous session (default: true) */
|
|
30
40
|
restoreContext?: boolean;
|
|
41
|
+
/** Auto-clone repository for project assignments */
|
|
42
|
+
autoSetup?: boolean;
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
export class AgentDaemon {
|
|
34
46
|
private agentName: string;
|
|
35
47
|
private config: Config;
|
|
36
48
|
private agentConfig: AgentConfig;
|
|
49
|
+
private runnerConfig: RunnerConfig;
|
|
37
50
|
private ws: AgentWebSocket | null = null;
|
|
38
51
|
private heartbeat: Heartbeat | null = null;
|
|
39
52
|
private token: string | null = null;
|
|
40
53
|
private agentId: string | null = null;
|
|
41
54
|
private isRunning = false;
|
|
42
55
|
private shouldRestoreContext: boolean;
|
|
56
|
+
private autoSetup: boolean;
|
|
43
57
|
|
|
44
58
|
constructor(options: DaemonOptions) {
|
|
45
59
|
const config = loadConfig();
|
|
@@ -50,6 +64,7 @@ export class AgentDaemon {
|
|
|
50
64
|
this.config = config;
|
|
51
65
|
this.agentName = options.name;
|
|
52
66
|
this.shouldRestoreContext = options.restoreContext !== false;
|
|
67
|
+
this.autoSetup = options.autoSetup === true;
|
|
53
68
|
|
|
54
69
|
// Find or create agent config
|
|
55
70
|
let agentConfig = config.agents.find((a) => a.name === options.name);
|
|
@@ -69,6 +84,18 @@ export class AgentDaemon {
|
|
|
69
84
|
if (options.model) agentConfig.model = options.model;
|
|
70
85
|
|
|
71
86
|
this.agentConfig = agentConfig;
|
|
87
|
+
|
|
88
|
+
// Build runner configuration with model resolution
|
|
89
|
+
this.runnerConfig = buildRunnerConfig({
|
|
90
|
+
cliModel: options.model,
|
|
91
|
+
agentModel: agentConfig.model,
|
|
92
|
+
defaultModel: config.defaults.model,
|
|
93
|
+
command: agentConfig.command,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const runnerName = getRunnerDisplayName(this.runnerConfig.type);
|
|
97
|
+
console.log(`Runner: ${runnerName}`);
|
|
98
|
+
console.log(`Effective model: ${this.runnerConfig.model}`);
|
|
72
99
|
}
|
|
73
100
|
|
|
74
101
|
async start(): Promise<void> {
|
|
@@ -79,6 +106,27 @@ export class AgentDaemon {
|
|
|
79
106
|
|
|
80
107
|
console.log(`Starting agent: ${this.agentName}`);
|
|
81
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
|
+
|
|
82
130
|
// Check if session already exists
|
|
83
131
|
const sessionName = getSessionName(this.agentName);
|
|
84
132
|
const sessionAlreadyExists = sessionExists(sessionName);
|
|
@@ -86,10 +134,13 @@ export class AgentDaemon {
|
|
|
86
134
|
// Create tmux session if it doesn't exist
|
|
87
135
|
if (!sessionAlreadyExists) {
|
|
88
136
|
console.log(`Creating tmux session: ${sessionName}`);
|
|
137
|
+
|
|
138
|
+
// Include runner env vars (e.g., OPENCODE_MODEL) at session creation
|
|
89
139
|
const created = createSession(
|
|
90
140
|
this.agentName,
|
|
91
141
|
this.agentConfig.command,
|
|
92
142
|
this.agentConfig.workdir,
|
|
143
|
+
this.runnerConfig.env, // Apply model env at process start
|
|
93
144
|
);
|
|
94
145
|
|
|
95
146
|
if (!created) {
|
|
@@ -97,26 +148,10 @@ export class AgentDaemon {
|
|
|
97
148
|
}
|
|
98
149
|
} else {
|
|
99
150
|
console.log(`Reconnecting to existing session: ${sessionName}`);
|
|
151
|
+
// Update environment for existing session
|
|
152
|
+
updateSessionEnvironment(this.agentName, this.runnerConfig.env);
|
|
100
153
|
}
|
|
101
154
|
|
|
102
|
-
// Register with hub
|
|
103
|
-
console.log("Registering with AgentMesh hub...");
|
|
104
|
-
const existingState = getAgentState(this.agentName);
|
|
105
|
-
|
|
106
|
-
const registration = await registerAgent({
|
|
107
|
-
url: this.config.hubUrl,
|
|
108
|
-
apiKey: this.config.apiKey,
|
|
109
|
-
workspace: this.config.workspace,
|
|
110
|
-
agentId: existingState?.agentId || this.agentConfig.agentId,
|
|
111
|
-
agentName: this.agentName,
|
|
112
|
-
model: this.agentConfig.model || this.config.defaults.model,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
this.agentId = registration.agentId;
|
|
116
|
-
this.token = registration.token;
|
|
117
|
-
|
|
118
|
-
console.log(`Registered as: ${this.agentId}`);
|
|
119
|
-
|
|
120
155
|
// Inject environment variables into tmux session
|
|
121
156
|
console.log("Injecting environment variables...");
|
|
122
157
|
updateSessionEnvironment(this.agentName, {
|
|
@@ -124,7 +159,7 @@ export class AgentDaemon {
|
|
|
124
159
|
AGENTMESH_AGENT_ID: this.agentId,
|
|
125
160
|
});
|
|
126
161
|
|
|
127
|
-
// Save state
|
|
162
|
+
// Save state including runtime model info
|
|
128
163
|
addAgentToState({
|
|
129
164
|
name: this.agentName,
|
|
130
165
|
agentId: this.agentId,
|
|
@@ -132,6 +167,8 @@ export class AgentDaemon {
|
|
|
132
167
|
tmuxSession: sessionName,
|
|
133
168
|
startedAt: new Date().toISOString(),
|
|
134
169
|
token: this.token,
|
|
170
|
+
runtimeModel: this.runnerConfig.model,
|
|
171
|
+
runnerType: this.runnerConfig.type,
|
|
135
172
|
});
|
|
136
173
|
|
|
137
174
|
// Start heartbeat with auto-refresh
|
|
@@ -314,4 +351,170 @@ Nudge agent:
|
|
|
314
351
|
console.error("Failed to save agent context:", error);
|
|
315
352
|
}
|
|
316
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
|
+
}
|
|
317
520
|
}
|
package/src/core/registry.ts
CHANGED
|
@@ -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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner Module
|
|
3
|
+
* Handles model resolution, validation, and runner-specific configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export type RunnerType = "opencode" | "claude" | "custom";
|
|
13
|
+
|
|
14
|
+
export interface RunnerConfig {
|
|
15
|
+
type: RunnerType;
|
|
16
|
+
command: string;
|
|
17
|
+
model: string;
|
|
18
|
+
env: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ModelResolutionInput {
|
|
22
|
+
cliModel?: string;
|
|
23
|
+
agentModel?: string;
|
|
24
|
+
defaultModel: string;
|
|
25
|
+
command: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Runner Detection
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detects the runner type from the command
|
|
34
|
+
*/
|
|
35
|
+
export function detectRunner(command: string): RunnerType {
|
|
36
|
+
const cmd = command.toLowerCase().trim();
|
|
37
|
+
|
|
38
|
+
if (cmd === "opencode" || cmd.startsWith("opencode ")) {
|
|
39
|
+
return "opencode";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (cmd === "claude" || cmd.startsWith("claude ")) {
|
|
43
|
+
return "claude";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return "custom";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Model Resolution
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolves the effective model from CLI > agent config > defaults
|
|
55
|
+
*/
|
|
56
|
+
export function resolveModel(input: ModelResolutionInput): string {
|
|
57
|
+
// Priority: CLI flag > agent config > default
|
|
58
|
+
return input.cliModel || input.agentModel || input.defaultModel;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// OpenCode Integration
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
let cachedOpenCodeModels: string[] | null = null;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Gets available OpenCode models (cached)
|
|
69
|
+
*/
|
|
70
|
+
export function getOpenCodeModels(): string[] {
|
|
71
|
+
if (cachedOpenCodeModels) {
|
|
72
|
+
return cachedOpenCodeModels;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const output = execSync("opencode models 2>/dev/null", {
|
|
77
|
+
encoding: "utf-8",
|
|
78
|
+
timeout: 10000,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
cachedOpenCodeModels = output
|
|
82
|
+
.split("\n")
|
|
83
|
+
.map((line) => line.trim())
|
|
84
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
85
|
+
|
|
86
|
+
return cachedOpenCodeModels;
|
|
87
|
+
} catch {
|
|
88
|
+
// OpenCode not available or failed
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validates that a model is available in OpenCode
|
|
95
|
+
*/
|
|
96
|
+
export function validateOpenCodeModel(model: string): { valid: boolean; error?: string } {
|
|
97
|
+
const models = getOpenCodeModels();
|
|
98
|
+
|
|
99
|
+
// If we couldn't get models list, allow any (graceful degradation)
|
|
100
|
+
if (models.length === 0) {
|
|
101
|
+
return { valid: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (models.includes(model)) {
|
|
105
|
+
return { valid: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for partial match (e.g., "claude-sonnet-4" matches "anthropic/claude-sonnet-4")
|
|
109
|
+
const partialMatch = models.find(
|
|
110
|
+
(m) => m.endsWith(`/${model}`) || m === model || m.split("/").pop() === model,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (partialMatch) {
|
|
114
|
+
return { valid: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
valid: false,
|
|
119
|
+
error: `Model "${model}" not found in OpenCode. Run 'opencode models' to see available models.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Normalizes a model name for OpenCode
|
|
125
|
+
* e.g., "claude-sonnet-4" -> "anthropic/claude-sonnet-4" if that's what OpenCode expects
|
|
126
|
+
*/
|
|
127
|
+
export function normalizeOpenCodeModel(model: string): string {
|
|
128
|
+
const models = getOpenCodeModels();
|
|
129
|
+
|
|
130
|
+
// Direct match
|
|
131
|
+
if (models.includes(model)) {
|
|
132
|
+
return model;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Try to find full path version
|
|
136
|
+
const fullPath = models.find((m) => m.endsWith(`/${model}`) || m.split("/").pop() === model);
|
|
137
|
+
|
|
138
|
+
return fullPath || model;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Runner Configuration
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Builds the complete runner configuration including environment variables
|
|
147
|
+
*/
|
|
148
|
+
export function buildRunnerConfig(input: ModelResolutionInput): RunnerConfig {
|
|
149
|
+
const runnerType = detectRunner(input.command);
|
|
150
|
+
const model = resolveModel(input);
|
|
151
|
+
const env: Record<string, string> = {};
|
|
152
|
+
|
|
153
|
+
switch (runnerType) {
|
|
154
|
+
case "opencode": {
|
|
155
|
+
// Validate model for OpenCode
|
|
156
|
+
const validation = validateOpenCodeModel(model);
|
|
157
|
+
if (!validation.valid) {
|
|
158
|
+
console.warn(`Warning: ${validation.error}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Normalize and set OPENCODE_MODEL
|
|
162
|
+
const normalizedModel = normalizeOpenCodeModel(model);
|
|
163
|
+
env.OPENCODE_MODEL = normalizedModel;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case "claude": {
|
|
168
|
+
// Claude CLI uses ANTHROPIC_MODEL or similar
|
|
169
|
+
// For now, just pass the model - Claude CLI will handle it
|
|
170
|
+
env.CLAUDE_MODEL = model;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case "custom":
|
|
175
|
+
// Custom runners don't get automatic model env
|
|
176
|
+
// User is responsible for configuring their tool
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
type: runnerType,
|
|
182
|
+
command: input.command,
|
|
183
|
+
model,
|
|
184
|
+
env,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Gets a human-readable runner name
|
|
190
|
+
*/
|
|
191
|
+
export function getRunnerDisplayName(runnerType: RunnerType): string {
|
|
192
|
+
switch (runnerType) {
|
|
193
|
+
case "opencode":
|
|
194
|
+
return "OpenCode";
|
|
195
|
+
case "claude":
|
|
196
|
+
return "Claude CLI";
|
|
197
|
+
case "custom":
|
|
198
|
+
return "Custom";
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/core/tmux.ts
CHANGED
|
@@ -18,6 +18,9 @@ export function sessionExists(sessionName: string): boolean {
|
|
|
18
18
|
export interface SessionEnv {
|
|
19
19
|
AGENT_TOKEN?: string;
|
|
20
20
|
AGENTMESH_AGENT_ID?: string;
|
|
21
|
+
OPENCODE_MODEL?: string;
|
|
22
|
+
CLAUDE_MODEL?: string;
|
|
23
|
+
[key: string]: string | undefined;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export function createSession(
|
|
@@ -34,17 +37,36 @@ export function createSession(
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
try {
|
|
40
|
+
// Build environment prefix for the command
|
|
41
|
+
// This ensures env vars are set BEFORE the process starts
|
|
42
|
+
let envPrefix = "";
|
|
43
|
+
if (env) {
|
|
44
|
+
const envParts: string[] = [];
|
|
45
|
+
for (const [key, value] of Object.entries(env)) {
|
|
46
|
+
if (value !== undefined && value !== "") {
|
|
47
|
+
// Escape special characters in value
|
|
48
|
+
const escapedValue = value.replace(/"/g, '\\"');
|
|
49
|
+
envParts.push(`${key}="${escapedValue}"`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (envParts.length > 0) {
|
|
53
|
+
envPrefix = envParts.join(" ") + " ";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fullCommand = `${envPrefix}${command}`;
|
|
58
|
+
|
|
37
59
|
const args = ["new-session", "-d", "-s", sessionName];
|
|
38
60
|
|
|
39
61
|
if (workdir) {
|
|
40
62
|
args.push("-c", workdir);
|
|
41
63
|
}
|
|
42
64
|
|
|
43
|
-
args.push(
|
|
65
|
+
args.push(fullCommand);
|
|
44
66
|
|
|
45
67
|
execSync(`tmux ${args.join(" ")}`);
|
|
46
68
|
|
|
47
|
-
//
|
|
69
|
+
// Also set session environment for any subsequent processes/refreshes
|
|
48
70
|
if (env) {
|
|
49
71
|
setSessionEnvironment(sessionName, env);
|
|
50
72
|
}
|
|
@@ -58,13 +80,10 @@ export function createSession(
|
|
|
58
80
|
|
|
59
81
|
export function setSessionEnvironment(sessionName: string, env: SessionEnv): boolean {
|
|
60
82
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
execSync(
|
|
66
|
-
`tmux set-environment -t "${sessionName}" AGENTMESH_AGENT_ID "${env.AGENTMESH_AGENT_ID}"`,
|
|
67
|
-
);
|
|
83
|
+
for (const [key, value] of Object.entries(env)) {
|
|
84
|
+
if (value !== undefined && value !== "") {
|
|
85
|
+
execSync(`tmux set-environment -t "${sessionName}" ${key} "${value}"`);
|
|
86
|
+
}
|
|
68
87
|
}
|
|
69
88
|
return true;
|
|
70
89
|
} catch (error) {
|