@agentmeshhq/agent 0.1.11 → 0.1.13
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__/injector.test.d.ts +1 -0
- package/dist/__tests__/injector.test.js +26 -0
- package/dist/__tests__/injector.test.js.map +1 -0
- package/dist/__tests__/sandbox.test.d.ts +1 -0
- package/dist/__tests__/sandbox.test.js +362 -0
- package/dist/__tests__/sandbox.test.js.map +1 -0
- package/dist/cli/build.d.ts +6 -0
- package/dist/cli/build.js +111 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/deploy.d.ts +9 -0
- package/dist/cli/deploy.js +130 -0
- package/dist/cli/deploy.js.map +1 -0
- package/dist/cli/inbox.d.ts +5 -0
- package/dist/cli/inbox.js +123 -0
- package/dist/cli/inbox.js.map +1 -0
- package/dist/cli/index.js +159 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/issue.d.ts +42 -0
- package/dist/cli/issue.js +297 -0
- package/dist/cli/issue.js.map +1 -0
- package/dist/cli/local.d.ts +9 -0
- package/dist/cli/local.js +139 -0
- package/dist/cli/local.js.map +1 -0
- package/dist/cli/migrate.d.ts +8 -0
- package/dist/cli/migrate.js +167 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli/ready.d.ts +5 -0
- package/dist/cli/ready.js +131 -0
- package/dist/cli/ready.js.map +1 -0
- package/dist/cli/slack.d.ts +3 -0
- package/dist/cli/slack.js +57 -0
- package/dist/cli/slack.js.map +1 -0
- package/dist/cli/start.d.ts +12 -0
- package/dist/cli/start.js +14 -0
- package/dist/cli/start.js.map +1 -1
- package/dist/cli/sync.d.ts +8 -0
- package/dist/cli/sync.js +154 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/cli/test.d.ts +8 -0
- package/dist/cli/test.js +110 -0
- package/dist/cli/test.js.map +1 -0
- package/dist/core/daemon.d.ts +31 -0
- package/dist/core/daemon.js +188 -28
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/injector.d.ts +6 -1
- package/dist/core/injector.js +64 -1
- package/dist/core/injector.js.map +1 -1
- package/dist/core/issue-cache.d.ts +44 -0
- package/dist/core/issue-cache.js +75 -0
- package/dist/core/issue-cache.js.map +1 -0
- package/dist/core/registry.d.ts +5 -0
- package/dist/core/registry.js +8 -1
- package/dist/core/registry.js.map +1 -1
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +377 -0
- package/dist/core/sandbox.js.map +1 -0
- package/dist/core/tmux.js +8 -10
- package/dist/core/tmux.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/injector.test.ts +29 -0
- package/src/__tests__/sandbox.test.ts +435 -0
- package/src/cli/build.ts +137 -0
- package/src/cli/deploy.ts +153 -0
- package/src/cli/index.ts +163 -0
- package/src/cli/local.ts +174 -0
- package/src/cli/migrate.ts +210 -0
- package/src/cli/slack.ts +69 -0
- package/src/cli/start.ts +22 -0
- package/src/cli/test.ts +141 -0
- package/src/core/daemon.ts +228 -37
- package/src/core/injector.ts +98 -1
- package/src/core/registry.ts +14 -1
- package/src/core/sandbox.ts +505 -0
- package/src/core/tmux.ts +9 -11
package/src/core/daemon.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { type ChildProcess, execSync, spawn } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
@@ -13,13 +13,14 @@ import type { AgentConfig, Config } from "../config/schema.js";
|
|
|
13
13
|
import { loadContext, loadOrCreateContext, saveContext } from "../context/index.js";
|
|
14
14
|
import { Heartbeat } from "./heartbeat.js";
|
|
15
15
|
import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
|
|
16
|
-
import { checkInbox, fetchAssignments, registerAgent } from "./registry.js";
|
|
16
|
+
import { checkInbox, fetchAssignments, registerAgent, type ServerContext } from "./registry.js";
|
|
17
17
|
import {
|
|
18
18
|
buildRunnerConfig,
|
|
19
19
|
detectRunner,
|
|
20
20
|
getRunnerDisplayName,
|
|
21
21
|
type RunnerConfig,
|
|
22
22
|
} from "./runner.js";
|
|
23
|
+
import { DockerSandbox } from "./sandbox.js";
|
|
23
24
|
import {
|
|
24
25
|
captureSessionContext,
|
|
25
26
|
createSession,
|
|
@@ -40,6 +41,18 @@ export interface DaemonOptions {
|
|
|
40
41
|
restoreContext?: boolean;
|
|
41
42
|
/** Auto-clone repository for project assignments */
|
|
42
43
|
autoSetup?: boolean;
|
|
44
|
+
/** Run opencode serve instead of tmux TUI (for Integration Service) */
|
|
45
|
+
serve?: boolean;
|
|
46
|
+
/** Port for opencode serve (default: 3001) */
|
|
47
|
+
servePort?: number;
|
|
48
|
+
/** Run agent in Docker sandbox container */
|
|
49
|
+
sandbox?: boolean;
|
|
50
|
+
/** Docker image for sandbox (default: agentmesh/agent-sandbox:latest) */
|
|
51
|
+
sandboxImage?: string;
|
|
52
|
+
/** CPU limit for sandbox (default: 1) */
|
|
53
|
+
sandboxCpu?: string;
|
|
54
|
+
/** Memory limit for sandbox (default: 2g) */
|
|
55
|
+
sandboxMemory?: string;
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
export class AgentDaemon {
|
|
@@ -55,6 +68,15 @@ export class AgentDaemon {
|
|
|
55
68
|
private assignedProject: string | undefined;
|
|
56
69
|
private shouldRestoreContext: boolean;
|
|
57
70
|
private autoSetup: boolean;
|
|
71
|
+
private serveMode: boolean;
|
|
72
|
+
private servePort: number;
|
|
73
|
+
private serveProcess: ChildProcess | null = null;
|
|
74
|
+
private serverContext: ServerContext | undefined;
|
|
75
|
+
private sandboxMode: boolean;
|
|
76
|
+
private sandboxImage: string;
|
|
77
|
+
private sandboxCpu: string;
|
|
78
|
+
private sandboxMemory: string;
|
|
79
|
+
private sandbox: DockerSandbox | null = null;
|
|
58
80
|
|
|
59
81
|
constructor(options: DaemonOptions) {
|
|
60
82
|
const config = loadConfig();
|
|
@@ -85,6 +107,12 @@ export class AgentDaemon {
|
|
|
85
107
|
if (options.model) agentConfig.model = options.model;
|
|
86
108
|
|
|
87
109
|
this.agentConfig = agentConfig;
|
|
110
|
+
this.serveMode = options.serve === true;
|
|
111
|
+
this.servePort = options.servePort || 3001;
|
|
112
|
+
this.sandboxMode = options.sandbox === true;
|
|
113
|
+
this.sandboxImage = options.sandboxImage || "agentmesh/agent-sandbox:latest";
|
|
114
|
+
this.sandboxCpu = options.sandboxCpu || "1";
|
|
115
|
+
this.sandboxMemory = options.sandboxMemory || "2g";
|
|
88
116
|
|
|
89
117
|
// Build runner configuration with model resolution
|
|
90
118
|
this.runnerConfig = buildRunnerConfig({
|
|
@@ -110,6 +138,7 @@ export class AgentDaemon {
|
|
|
110
138
|
// Register with hub first (needed for assignment check)
|
|
111
139
|
console.log("Registering with AgentMesh hub...");
|
|
112
140
|
const existingState = getAgentState(this.agentName);
|
|
141
|
+
console.log(`Existing state: ${existingState ? `agentId=${existingState.agentId}` : "none"}`);
|
|
113
142
|
|
|
114
143
|
const registration = await registerAgent({
|
|
115
144
|
url: this.config.hubUrl,
|
|
@@ -118,49 +147,66 @@ export class AgentDaemon {
|
|
|
118
147
|
agentId: existingState?.agentId || this.agentConfig.agentId,
|
|
119
148
|
agentName: this.agentName,
|
|
120
149
|
model: this.agentConfig.model || this.config.defaults.model,
|
|
150
|
+
restoreContext: this.shouldRestoreContext,
|
|
121
151
|
});
|
|
122
152
|
|
|
123
153
|
this.agentId = registration.agentId;
|
|
124
154
|
this.token = registration.token;
|
|
125
155
|
|
|
126
|
-
|
|
156
|
+
if (registration.status === "re-registered") {
|
|
157
|
+
console.log(`Re-registered as: ${this.agentId}`);
|
|
158
|
+
if (registration.context && Object.keys(registration.context).length > 0) {
|
|
159
|
+
this.serverContext = registration.context;
|
|
160
|
+
console.log(`Server context restored: ${Object.keys(registration.context).join(", ")}`);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`Registered as: ${this.agentId}`);
|
|
164
|
+
}
|
|
127
165
|
|
|
128
166
|
// Check assignments and auto-setup workdir if needed (before creating tmux session)
|
|
129
167
|
await this.checkAssignments();
|
|
130
168
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!sessionAlreadyExists) {
|
|
137
|
-
console.log(`Creating tmux session: ${sessionName}`);
|
|
138
|
-
|
|
139
|
-
// Include runner env vars (e.g., OPENCODE_MODEL) at session creation
|
|
140
|
-
const created = createSession(
|
|
141
|
-
this.agentName,
|
|
142
|
-
this.agentConfig.command,
|
|
143
|
-
this.agentConfig.workdir,
|
|
144
|
-
this.runnerConfig.env, // Apply model env at process start
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
if (!created) {
|
|
148
|
-
throw new Error("Failed to create tmux session");
|
|
149
|
-
}
|
|
169
|
+
// Choose runtime mode: sandbox > serve > tmux
|
|
170
|
+
if (this.sandboxMode) {
|
|
171
|
+
await this.startSandboxMode();
|
|
172
|
+
} else if (this.serveMode) {
|
|
173
|
+
await this.startServeMode();
|
|
150
174
|
} else {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
// Check if session already exists
|
|
176
|
+
const sessionName = getSessionName(this.agentName);
|
|
177
|
+
const sessionAlreadyExists = sessionExists(sessionName);
|
|
178
|
+
|
|
179
|
+
// Create tmux session if it doesn't exist
|
|
180
|
+
if (!sessionAlreadyExists) {
|
|
181
|
+
console.log(`Creating tmux session: ${sessionName}`);
|
|
182
|
+
|
|
183
|
+
// Include runner env vars (e.g., OPENCODE_MODEL) at session creation
|
|
184
|
+
const created = createSession(
|
|
185
|
+
this.agentName,
|
|
186
|
+
this.agentConfig.command,
|
|
187
|
+
this.agentConfig.workdir,
|
|
188
|
+
this.runnerConfig.env, // Apply model env at process start
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!created) {
|
|
192
|
+
throw new Error("Failed to create tmux session");
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
console.log(`Reconnecting to existing session: ${sessionName}`);
|
|
196
|
+
// Update environment for existing session
|
|
197
|
+
updateSessionEnvironment(this.agentName, this.runnerConfig.env);
|
|
198
|
+
}
|
|
155
199
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
200
|
+
// Inject environment variables into tmux session
|
|
201
|
+
console.log("Injecting environment variables...");
|
|
202
|
+
updateSessionEnvironment(this.agentName, {
|
|
203
|
+
AGENT_TOKEN: this.token,
|
|
204
|
+
AGENTMESH_AGENT_ID: this.agentId,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
162
207
|
|
|
163
208
|
// Save state including runtime model info
|
|
209
|
+
const sessionName = this.serveMode ? `serve:${this.servePort}` : getSessionName(this.agentName);
|
|
164
210
|
addAgentToState({
|
|
165
211
|
name: this.agentName,
|
|
166
212
|
agentId: this.agentId,
|
|
@@ -211,6 +257,7 @@ export class AgentDaemon {
|
|
|
211
257
|
url: `${wsUrl}/ws/v1`,
|
|
212
258
|
token: newToken,
|
|
213
259
|
onMessage: (event) => {
|
|
260
|
+
console.log(`[WS] Received event: ${event.type}`);
|
|
214
261
|
handleWebSocketEvent(this.agentName, event);
|
|
215
262
|
},
|
|
216
263
|
onConnect: () => {
|
|
@@ -237,7 +284,11 @@ export class AgentDaemon {
|
|
|
237
284
|
url: `${wsUrl}/ws/v1`,
|
|
238
285
|
token: this.token,
|
|
239
286
|
onMessage: (event) => {
|
|
240
|
-
|
|
287
|
+
console.log(`[WS] Received event: ${event.type}`);
|
|
288
|
+
handleWebSocketEvent(this.agentName, event, {
|
|
289
|
+
hubUrl: this.config.hubUrl,
|
|
290
|
+
token: this.token ?? undefined,
|
|
291
|
+
});
|
|
241
292
|
},
|
|
242
293
|
onConnect: () => {
|
|
243
294
|
console.log("WebSocket connected");
|
|
@@ -316,16 +367,156 @@ Nudge agent:
|
|
|
316
367
|
this.ws = null;
|
|
317
368
|
}
|
|
318
369
|
|
|
319
|
-
//
|
|
320
|
-
|
|
370
|
+
// Stop sandbox, serve process, or destroy tmux session
|
|
371
|
+
if (this.sandboxMode && this.sandbox) {
|
|
372
|
+
console.log("Stopping sandbox container...");
|
|
373
|
+
await this.sandbox.destroy();
|
|
374
|
+
this.sandbox = null;
|
|
375
|
+
} else if (this.serveMode && this.serveProcess) {
|
|
376
|
+
console.log("Stopping opencode serve...");
|
|
377
|
+
this.serveProcess.kill("SIGTERM");
|
|
378
|
+
this.serveProcess = null;
|
|
379
|
+
} else {
|
|
380
|
+
destroySession(this.agentName);
|
|
381
|
+
}
|
|
321
382
|
|
|
322
|
-
//
|
|
323
|
-
|
|
383
|
+
// Update state to mark as stopped but preserve agentId for next restart
|
|
384
|
+
updateAgentInState(this.agentName, {
|
|
385
|
+
pid: 0,
|
|
386
|
+
tmuxSession: "",
|
|
387
|
+
startedAt: "",
|
|
388
|
+
token: undefined,
|
|
389
|
+
});
|
|
324
390
|
|
|
325
391
|
console.log("Agent stopped.");
|
|
326
392
|
process.exit(0);
|
|
327
393
|
}
|
|
328
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Starts opencode serve mode (for Integration Service)
|
|
397
|
+
* Replaces tmux with a direct HTTP server
|
|
398
|
+
*/
|
|
399
|
+
private async startServeMode(): Promise<void> {
|
|
400
|
+
console.log(`Starting opencode serve mode on port ${this.servePort}...`);
|
|
401
|
+
|
|
402
|
+
const workdir = this.agentConfig.workdir || process.cwd();
|
|
403
|
+
|
|
404
|
+
// Build environment for opencode serve
|
|
405
|
+
const env: Record<string, string> = {
|
|
406
|
+
...process.env,
|
|
407
|
+
...this.runnerConfig.env,
|
|
408
|
+
AGENT_TOKEN: this.token!,
|
|
409
|
+
AGENTMESH_AGENT_ID: this.agentId!,
|
|
410
|
+
} as Record<string, string>;
|
|
411
|
+
|
|
412
|
+
// Spawn opencode serve as a child process
|
|
413
|
+
this.serveProcess = spawn(
|
|
414
|
+
"opencode",
|
|
415
|
+
["serve", "--port", String(this.servePort), "--hostname", "0.0.0.0"],
|
|
416
|
+
{
|
|
417
|
+
cwd: workdir,
|
|
418
|
+
env,
|
|
419
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// Handle process exit
|
|
424
|
+
this.serveProcess.on("exit", (code, signal) => {
|
|
425
|
+
console.error(`opencode serve exited with code ${code}, signal ${signal}`);
|
|
426
|
+
if (this.isRunning) {
|
|
427
|
+
console.log("Restarting opencode serve in 5 seconds...");
|
|
428
|
+
setTimeout(() => {
|
|
429
|
+
if (this.isRunning) {
|
|
430
|
+
this.startServeMode().catch(console.error);
|
|
431
|
+
}
|
|
432
|
+
}, 5000);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
this.serveProcess.on("error", (error) => {
|
|
437
|
+
console.error("Failed to start opencode serve:", error);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Wait a moment for the server to start
|
|
441
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
442
|
+
|
|
443
|
+
console.log(`opencode serve started on http://0.0.0.0:${this.servePort}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Starts agent in Docker sandbox mode
|
|
448
|
+
* Provides filesystem isolation with only workspace mounted
|
|
449
|
+
*/
|
|
450
|
+
private async startSandboxMode(): Promise<void> {
|
|
451
|
+
console.log("Starting in Docker sandbox mode...");
|
|
452
|
+
|
|
453
|
+
// Check Docker availability
|
|
454
|
+
if (!DockerSandbox.checkDockerAvailable()) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
"Docker is not available. Install Docker or use --sandbox host to run on host.",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const workdir = this.agentConfig.workdir || process.cwd();
|
|
461
|
+
|
|
462
|
+
// Check for existing sandbox container
|
|
463
|
+
const existingContainer = DockerSandbox.findExisting(this.agentName);
|
|
464
|
+
if (existingContainer) {
|
|
465
|
+
console.log(`Found existing sandbox container: ${existingContainer}`);
|
|
466
|
+
console.log("Stop it with: agentmesh stop " + this.agentName);
|
|
467
|
+
throw new Error("Sandbox container already exists");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Create sandbox configuration
|
|
471
|
+
this.sandbox = new DockerSandbox({
|
|
472
|
+
agentName: this.agentName,
|
|
473
|
+
image: this.sandboxImage,
|
|
474
|
+
workspacePath: workdir,
|
|
475
|
+
cpuLimit: this.sandboxCpu,
|
|
476
|
+
memoryLimit: this.sandboxMemory,
|
|
477
|
+
env: {
|
|
478
|
+
...this.runnerConfig.env,
|
|
479
|
+
AGENT_TOKEN: this.token!,
|
|
480
|
+
AGENTMESH_AGENT_ID: this.agentId!,
|
|
481
|
+
},
|
|
482
|
+
serveMode: this.serveMode,
|
|
483
|
+
servePort: this.servePort,
|
|
484
|
+
networkMode: "bridge",
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Validate mount policy (will throw if denied)
|
|
488
|
+
this.sandbox.validateMountPolicy();
|
|
489
|
+
|
|
490
|
+
// Pull image if needed
|
|
491
|
+
await this.sandbox.pullImage();
|
|
492
|
+
|
|
493
|
+
// Start container
|
|
494
|
+
await this.sandbox.start();
|
|
495
|
+
|
|
496
|
+
console.log(`
|
|
497
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
498
|
+
🐳 SANDBOX MODE ACTIVE
|
|
499
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
500
|
+
|
|
501
|
+
Container: ${this.sandbox.getContainerName()}
|
|
502
|
+
Image: ${this.sandboxImage}
|
|
503
|
+
Workspace: ${workdir} -> /workspace
|
|
504
|
+
CPU: ${this.sandboxCpu} core(s)
|
|
505
|
+
Memory: ${this.sandboxMemory}
|
|
506
|
+
|
|
507
|
+
The agent is running in an isolated Docker container.
|
|
508
|
+
Only the workspace directory is accessible.
|
|
509
|
+
|
|
510
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
511
|
+
`);
|
|
512
|
+
|
|
513
|
+
// Start opencode in the container
|
|
514
|
+
if (!this.serveMode) {
|
|
515
|
+
console.log("Starting opencode in sandbox container...");
|
|
516
|
+
await this.sandbox.spawnOpencode();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
329
520
|
/**
|
|
330
521
|
* Saves the current agent context to disk
|
|
331
522
|
*/
|
package/src/core/injector.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { formatHandoffContextSummary, parseHandoffContext } from "../context/handoff.js";
|
|
2
4
|
import type { AgentContext } from "../context/schema.js";
|
|
3
5
|
import type { InboxItem } from "./registry.js";
|
|
@@ -65,6 +67,85 @@ You can now proceed with your work.`;
|
|
|
65
67
|
sendKeys(agentName, message);
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
export interface EventContext {
|
|
71
|
+
hubUrl?: string;
|
|
72
|
+
token?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface SlackFile {
|
|
76
|
+
name: string;
|
|
77
|
+
type: string;
|
|
78
|
+
url: string;
|
|
79
|
+
permalink: string;
|
|
80
|
+
base64?: string;
|
|
81
|
+
mediaType?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function injectSlackMessage(
|
|
85
|
+
agentName: string,
|
|
86
|
+
event: WebSocketEvent,
|
|
87
|
+
context?: EventContext,
|
|
88
|
+
): void {
|
|
89
|
+
const user = (event.user as string) || "unknown";
|
|
90
|
+
const channel = (event.channel as string) || "unknown";
|
|
91
|
+
const text = (event.text as string) || "";
|
|
92
|
+
const files = (event.files as SlackFile[] | undefined) || [];
|
|
93
|
+
|
|
94
|
+
// Build message with file info
|
|
95
|
+
let message = `[Slack from ${user} in ${channel}] ${text}`;
|
|
96
|
+
|
|
97
|
+
if (files.length > 0) {
|
|
98
|
+
const savedImages: string[] = [];
|
|
99
|
+
const otherFiles: string[] = [];
|
|
100
|
+
|
|
101
|
+
// Save images with base64 data to disk
|
|
102
|
+
const attachmentsDir = join(process.env.HOME || "/tmp", ".agentmesh", "attachments");
|
|
103
|
+
if (!existsSync(attachmentsDir)) {
|
|
104
|
+
mkdirSync(attachmentsDir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const f of files) {
|
|
108
|
+
console.log(
|
|
109
|
+
`[Injector] File: ${f.name}, type: ${f.type}, mediaType: ${f.mediaType}, has base64: ${!!f.base64}`,
|
|
110
|
+
);
|
|
111
|
+
if (f.base64 && (f.mediaType?.startsWith("image/") || f.type?.startsWith("image/"))) {
|
|
112
|
+
// Save image to disk
|
|
113
|
+
const timestamp = Date.now();
|
|
114
|
+
const safeName = f.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
115
|
+
const filePath = join(attachmentsDir, `${timestamp}-${safeName}`);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
writeFileSync(filePath, Buffer.from(f.base64, "base64"));
|
|
119
|
+
savedImages.push(filePath);
|
|
120
|
+
console.log(`[Injector] Saved image to: ${filePath}`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error(`[Injector] Failed to save image:`, err);
|
|
123
|
+
otherFiles.push(`${f.name} (failed to save)`);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Just reference the URL for non-image files
|
|
127
|
+
otherFiles.push(`${f.name}: ${f.permalink}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add instruction to view images - OpenCode needs explicit instruction
|
|
132
|
+
if (savedImages.length > 0) {
|
|
133
|
+
message += `\n\nUser attached ${savedImages.length} image(s). Use the Read tool to view them:`;
|
|
134
|
+
for (const img of savedImages) {
|
|
135
|
+
message += `\n- ${img}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (otherFiles.length > 0) {
|
|
140
|
+
message += ` [Other files: ${otherFiles.join(" | ")}]`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`[Injector] Sending to tmux: ${message}`);
|
|
145
|
+
const result = sendKeys(agentName, message);
|
|
146
|
+
console.log(`[Injector] sendKeys result: ${result}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
68
149
|
export function injectInboxItems(agentName: string, items: InboxItem[]): void {
|
|
69
150
|
if (items.length === 0) {
|
|
70
151
|
sendKeys(agentName, "[AgentMesh] Your inbox is empty.");
|
|
@@ -85,7 +166,11 @@ export function injectInboxItems(agentName: string, items: InboxItem[]): void {
|
|
|
85
166
|
sendKeys(agentName, message);
|
|
86
167
|
}
|
|
87
168
|
|
|
88
|
-
export function handleWebSocketEvent(
|
|
169
|
+
export function handleWebSocketEvent(
|
|
170
|
+
agentName: string,
|
|
171
|
+
event: WebSocketEvent,
|
|
172
|
+
context?: EventContext,
|
|
173
|
+
): void {
|
|
89
174
|
switch (event.type) {
|
|
90
175
|
case "handoff_received":
|
|
91
176
|
case "handoff.received":
|
|
@@ -102,6 +187,18 @@ export function handleWebSocketEvent(agentName: string, event: WebSocketEvent):
|
|
|
102
187
|
injectBlockerResolved(agentName, event);
|
|
103
188
|
break;
|
|
104
189
|
|
|
190
|
+
case "slack.message":
|
|
191
|
+
console.log(`[Injector] Handling slack.message for ${agentName}`);
|
|
192
|
+
console.log(`[Injector] Event keys: ${Object.keys(event).join(", ")}`);
|
|
193
|
+
if (event.files) {
|
|
194
|
+
const files = event.files as Array<{ name: string; base64?: string }>;
|
|
195
|
+
console.log(
|
|
196
|
+
`[Injector] Files count: ${files.length}, has base64: ${files.some((f) => !!f.base64)}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
injectSlackMessage(agentName, event, context);
|
|
200
|
+
break;
|
|
201
|
+
|
|
105
202
|
default:
|
|
106
203
|
// Unknown event type, ignore
|
|
107
204
|
break;
|
package/src/core/registry.ts
CHANGED
|
@@ -8,11 +8,17 @@ export interface RegisterOptions {
|
|
|
8
8
|
agentName: string;
|
|
9
9
|
model: string;
|
|
10
10
|
capabilities?: string[];
|
|
11
|
+
restoreContext?: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
export type ServerContext = Record<string, Record<string, unknown>>;
|
|
15
|
+
|
|
13
16
|
export interface RegisterResult {
|
|
14
17
|
agentId: string;
|
|
15
18
|
token: string;
|
|
19
|
+
status?: "registered" | "re-registered";
|
|
20
|
+
context?: ServerContext;
|
|
21
|
+
assignments?: ProjectAssignment[];
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export interface InboxItem {
|
|
@@ -42,6 +48,7 @@ export async function registerAgent(options: RegisterOptions): Promise<RegisterR
|
|
|
42
48
|
model: options.model,
|
|
43
49
|
capabilities: options.capabilities || ["coding", "review", "debugging"],
|
|
44
50
|
workspace: options.workspace,
|
|
51
|
+
restore_context: options.restoreContext ?? true,
|
|
45
52
|
}),
|
|
46
53
|
});
|
|
47
54
|
|
|
@@ -57,7 +64,13 @@ export async function registerAgent(options: RegisterOptions): Promise<RegisterR
|
|
|
57
64
|
throw new Error("No token in registration response");
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
agentId,
|
|
69
|
+
token,
|
|
70
|
+
status: data.status,
|
|
71
|
+
context: data.context,
|
|
72
|
+
assignments: data.assignments,
|
|
73
|
+
};
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
export async function checkInbox(
|