fellow-agents 0.0.5
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/README.md +76 -0
- package/dist/cli.js +45 -0
- package/dist/commands/start.js +135 -0
- package/dist/commands/stop.js +8 -0
- package/dist/lib/download.js +93 -0
- package/dist/lib/paths.js +19 -0
- package/dist/lib/platform.js +15 -0
- package/dist/lib/services.js +97 -0
- package/dist/lib/workspaces.js +54 -0
- package/package.json +36 -0
- package/templates/coder/CLAUDE.md +15 -0
- package/templates/coder/identity.json +1 -0
- package/templates/coordinator/CLAUDE.md +22 -0
- package/templates/coordinator/identity.json +1 -0
- package/templates/reviewer/CLAUDE.md +15 -0
- package/templates/reviewer/identity.json +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# fellow-agents
|
|
2
|
+
|
|
3
|
+
Multiple Claude Code agents collaborating via messaging.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g fellow-agents
|
|
9
|
+
fellow-agents start
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Downloads binaries on first run, starts services, opens browser. No git clone needed.
|
|
13
|
+
|
|
14
|
+
### Options
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
fellow-agents start --port 3700 --emcom-port 8800 # custom ports
|
|
18
|
+
fellow-agents start --no-browser # headless
|
|
19
|
+
fellow-agents start --update # force re-download binaries
|
|
20
|
+
fellow-agents stop # stop all services
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- [Node.js](https://nodejs.org/) 18+
|
|
26
|
+
- [Claude Code](https://claude.ai/code) (optional for setup, required to run agents)
|
|
27
|
+
|
|
28
|
+
## Alternative: Git Clone
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/rajan-chari/fellow-agents.git
|
|
32
|
+
cd fellow-agents
|
|
33
|
+
./setup.sh # Mac/Linux
|
|
34
|
+
pwsh ./setup.ps1 # Windows (requires PowerShell 7+)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Try It
|
|
38
|
+
|
|
39
|
+
1. Open **coordinator** in pty-win (click play)
|
|
40
|
+
2. Say: *"Have the coder write a fibonacci script and the reviewer check it."*
|
|
41
|
+
3. Watch agents message each other in the emcom feed (right panel)
|
|
42
|
+
|
|
43
|
+
## What's Included
|
|
44
|
+
|
|
45
|
+
| Component | Purpose |
|
|
46
|
+
|-----------|---------|
|
|
47
|
+
| **pty-win** | Browser terminal multiplexer — manage all agent sessions |
|
|
48
|
+
| **emcom** | Async messaging between agents |
|
|
49
|
+
| **templates/** | 3 starter agents: coordinator, coder, reviewer |
|
|
50
|
+
|
|
51
|
+
## Add an Agent
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
mkdir workspaces/myagent
|
|
55
|
+
# Copy CLAUDE.md + identity.json from an existing agent, customize
|
|
56
|
+
emcom --identity workspaces/myagent/identity.json register
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Architecture
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
~/.fellow-agents/ # Data directory (auto-created)
|
|
63
|
+
bin/{platform}/ # emcom, tracker, emcom-server binaries
|
|
64
|
+
pty-win/ # Terminal multiplexer
|
|
65
|
+
pid/ # PID files for running services
|
|
66
|
+
logs/ # Service logs
|
|
67
|
+
|
|
68
|
+
./workspaces/ # Agent workspaces (scaffolded from templates)
|
|
69
|
+
coordinator/ # Task coordinator
|
|
70
|
+
coder/ # Code writer
|
|
71
|
+
reviewer/ # Code reviewer
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Stop
|
|
75
|
+
|
|
76
|
+
`fellow-agents stop` or `Ctrl+C` in the start terminal.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const args = process.argv.slice(2);
|
|
3
|
+
const command = args[0] || "start";
|
|
4
|
+
function getFlag(name, fallback) {
|
|
5
|
+
const idx = args.indexOf(name);
|
|
6
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback;
|
|
7
|
+
}
|
|
8
|
+
function hasFlag(name) {
|
|
9
|
+
return args.includes(name);
|
|
10
|
+
}
|
|
11
|
+
if (command === "start") {
|
|
12
|
+
const { start } = await import("./commands/start.js");
|
|
13
|
+
await start({
|
|
14
|
+
port: parseInt(getFlag("--port", "3700"), 10),
|
|
15
|
+
emcomPort: parseInt(getFlag("--emcom-port", "8800"), 10),
|
|
16
|
+
dir: getFlag("--dir", process.cwd()),
|
|
17
|
+
noBrowser: hasFlag("--no-browser"),
|
|
18
|
+
update: hasFlag("--update"),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
else if (command === "stop") {
|
|
22
|
+
const { stop } = await import("./commands/stop.js");
|
|
23
|
+
stop();
|
|
24
|
+
}
|
|
25
|
+
else if (command === "--help" || command === "-h") {
|
|
26
|
+
console.log(`fellow-agents — multi-agent system for Claude Code
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
fellow-agents start [options] Start services (download binaries on first run)
|
|
30
|
+
fellow-agents stop Stop all running services
|
|
31
|
+
|
|
32
|
+
Options (start):
|
|
33
|
+
--port <number> pty-win port (default: 3700)
|
|
34
|
+
--emcom-port <number> emcom-server port (default: 8800)
|
|
35
|
+
--dir <path> Working directory (default: current)
|
|
36
|
+
--no-browser Don't open browser
|
|
37
|
+
--update Force re-download binaries
|
|
38
|
+
|
|
39
|
+
-h, --help Show this help`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.error(`Unknown command: ${command}. Run 'fellow-agents --help' for usage.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { binDir, ptyWinDir, logsDir } from "../lib/paths.js";
|
|
6
|
+
import { downloadBinaries } from "../lib/download.js";
|
|
7
|
+
import { startEmcomServer, startPtyWin, stopAll, logPath } from "../lib/services.js";
|
|
8
|
+
import { scaffoldWorkspaces, registerAgents, writeHooks } from "../lib/workspaces.js";
|
|
9
|
+
import { binarySuffix } from "../lib/platform.js";
|
|
10
|
+
export async function start(opts) {
|
|
11
|
+
console.log("");
|
|
12
|
+
console.log(" fellow-agents");
|
|
13
|
+
console.log(" =============");
|
|
14
|
+
console.log("");
|
|
15
|
+
const workDir = resolve(opts.dir);
|
|
16
|
+
const workspacesDir = join(workDir, "workspaces");
|
|
17
|
+
const emcomUrl = `http://127.0.0.1:${opts.emcomPort}`;
|
|
18
|
+
// 1. Prerequisites
|
|
19
|
+
console.log("[1/7] Checking prerequisites...");
|
|
20
|
+
const nodeVer = process.versions.node;
|
|
21
|
+
const nodeMajor = parseInt(nodeVer.split(".")[0], 10);
|
|
22
|
+
if (nodeMajor < 18) {
|
|
23
|
+
console.error(` Node.js 18+ required (found ${nodeVer})`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
console.log(` Node.js ${nodeVer}`);
|
|
27
|
+
try {
|
|
28
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
29
|
+
console.log(" Claude Code found");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
console.log(" Claude Code not found (optional — install from https://claude.ai/code)");
|
|
33
|
+
}
|
|
34
|
+
// 2. Download binaries
|
|
35
|
+
console.log("[2/7] Downloading binaries...");
|
|
36
|
+
try {
|
|
37
|
+
await downloadBinaries(opts.update);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error(` Download failed: ${err}`);
|
|
41
|
+
if (!existsSync(join(binDir, `emcom${binarySuffix()}`))) {
|
|
42
|
+
console.error(" No cached binaries available. Check your internet connection.");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
console.log(" Using cached binaries");
|
|
46
|
+
}
|
|
47
|
+
// 3. Install pty-win dependencies
|
|
48
|
+
console.log("[3/7] Installing pty-win...");
|
|
49
|
+
if (existsSync(join(ptyWinDir, "package.json"))) {
|
|
50
|
+
if (!existsSync(join(ptyWinDir, "node_modules"))) {
|
|
51
|
+
execSync("npm install --production", { cwd: ptyWinDir, stdio: "pipe" });
|
|
52
|
+
}
|
|
53
|
+
console.log(" pty-win ready");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.error(" pty-win not found — download may have failed");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
// 4. Scaffold workspaces
|
|
60
|
+
console.log("[4/7] Scaffolding workspaces...");
|
|
61
|
+
scaffoldWorkspaces(workDir);
|
|
62
|
+
// PATH trick: prepend bin dir so agents find emcom/tracker
|
|
63
|
+
const env = { ...process.env, PATH: `${binDir}${process.platform === "win32" ? ";" : ":"}${process.env.PATH}` };
|
|
64
|
+
// 5. Start emcom-server
|
|
65
|
+
console.log("[5/7] Starting emcom-server...");
|
|
66
|
+
const emcomPid = startEmcomServer(opts.emcomPort, env);
|
|
67
|
+
console.log(` emcom-server started (pid ${emcomPid})`);
|
|
68
|
+
// Wait for health
|
|
69
|
+
const healthy = await new Promise((resolve) => {
|
|
70
|
+
let attempts = 0;
|
|
71
|
+
const check = () => {
|
|
72
|
+
http.get(`${emcomUrl}/api/health`, (res) => {
|
|
73
|
+
if (res.statusCode === 200)
|
|
74
|
+
return resolve(true);
|
|
75
|
+
if (++attempts < 20)
|
|
76
|
+
setTimeout(check, 500);
|
|
77
|
+
else
|
|
78
|
+
resolve(false);
|
|
79
|
+
}).on("error", () => {
|
|
80
|
+
if (++attempts < 20)
|
|
81
|
+
setTimeout(check, 500);
|
|
82
|
+
else
|
|
83
|
+
resolve(false);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
check();
|
|
87
|
+
});
|
|
88
|
+
if (healthy) {
|
|
89
|
+
console.log(` emcom-server running on :${opts.emcomPort}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.error(` Warning: emcom-server health check failed — it may not be running`);
|
|
93
|
+
console.error(` Check logs: ${logPath("emcom-server")}`);
|
|
94
|
+
}
|
|
95
|
+
// 6. Register agents
|
|
96
|
+
console.log("[6/7] Registering agents + configuring hooks...");
|
|
97
|
+
registerAgents(workspacesDir, env);
|
|
98
|
+
writeHooks(workspacesDir, opts.port);
|
|
99
|
+
// 7. Start pty-win
|
|
100
|
+
console.log("[7/7] Starting pty-win...");
|
|
101
|
+
const ptyPid = startPtyWin(opts.port, workspacesDir, emcomUrl, env);
|
|
102
|
+
console.log(` pty-win started (pid ${ptyPid})`);
|
|
103
|
+
// Open browser
|
|
104
|
+
if (!opts.noBrowser) {
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
const url = `http://127.0.0.1:${opts.port}`;
|
|
107
|
+
try {
|
|
108
|
+
if (process.platform === "win32")
|
|
109
|
+
execSync(`start "" "${url}"`, { stdio: "ignore" });
|
|
110
|
+
else if (process.platform === "darwin")
|
|
111
|
+
execSync(`open "${url}"`, { stdio: "ignore" });
|
|
112
|
+
else
|
|
113
|
+
execSync(`xdg-open "${url}" 2>/dev/null || true`, { stdio: "ignore" });
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
}, 2000);
|
|
117
|
+
}
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log(" Setup complete!");
|
|
120
|
+
console.log(` pty-win: http://127.0.0.1:${opts.port}`);
|
|
121
|
+
console.log(` emcom-server: ${emcomUrl}`);
|
|
122
|
+
console.log(` logs: ${logsDir}`);
|
|
123
|
+
console.log("");
|
|
124
|
+
console.log(" Press Ctrl+C to stop all services.");
|
|
125
|
+
console.log("");
|
|
126
|
+
// Wait for Ctrl+C
|
|
127
|
+
process.on("SIGINT", () => {
|
|
128
|
+
console.log("\n Shutting down...");
|
|
129
|
+
stopAll();
|
|
130
|
+
console.log(" Stopped.");
|
|
131
|
+
process.exit(0);
|
|
132
|
+
});
|
|
133
|
+
// Keep alive
|
|
134
|
+
setInterval(() => { }, 60000);
|
|
135
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { get } from "https";
|
|
2
|
+
import { createWriteStream, mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { dataDir, binDir, ptyWinDir, versionFile } from "./paths.js";
|
|
6
|
+
import { detectPlatform } from "./platform.js";
|
|
7
|
+
const REPO = "rajan-chari/fellow-agents";
|
|
8
|
+
function httpsGet(url) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
get(url, { headers: { "User-Agent": "fellow-agents-cli" } }, (res) => {
|
|
11
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
12
|
+
return httpsGet(res.headers.location).then(resolve, reject);
|
|
13
|
+
}
|
|
14
|
+
let data = "";
|
|
15
|
+
res.on("data", (chunk) => (data += chunk));
|
|
16
|
+
res.on("end", () => resolve(data));
|
|
17
|
+
res.on("error", reject);
|
|
18
|
+
}).on("error", reject);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function httpsDownload(url, dest) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
get(url, { headers: { "User-Agent": "fellow-agents-cli" } }, (res) => {
|
|
24
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
25
|
+
return httpsDownload(res.headers.location, dest).then(resolve, reject);
|
|
26
|
+
}
|
|
27
|
+
const file = createWriteStream(dest);
|
|
28
|
+
res.pipe(file);
|
|
29
|
+
file.on("finish", () => { file.close(); resolve(); });
|
|
30
|
+
file.on("error", reject);
|
|
31
|
+
}).on("error", reject);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export async function downloadBinaries(force = false) {
|
|
35
|
+
// Fetch latest release
|
|
36
|
+
const json = await httpsGet(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
37
|
+
const release = JSON.parse(json);
|
|
38
|
+
const tag = release.tag_name;
|
|
39
|
+
// Check if up to date
|
|
40
|
+
if (!force && existsSync(versionFile) && existsSync(binDir)) {
|
|
41
|
+
const localVer = readFileSync(versionFile, "utf-8").trim();
|
|
42
|
+
if (localVer === tag) {
|
|
43
|
+
console.log(` Binaries up to date (${tag})`);
|
|
44
|
+
return tag;
|
|
45
|
+
}
|
|
46
|
+
console.log(` Update available: ${localVer} → ${tag}`);
|
|
47
|
+
}
|
|
48
|
+
console.log(` Downloading release ${tag}...`);
|
|
49
|
+
const platform = detectPlatform();
|
|
50
|
+
for (const asset of release.assets) {
|
|
51
|
+
if (asset.name.includes(platform)) {
|
|
52
|
+
console.log(` Downloading ${asset.name}...`);
|
|
53
|
+
const dest = join(dataDir, asset.name);
|
|
54
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
55
|
+
await httpsDownload(asset.browser_download_url, dest);
|
|
56
|
+
// Extract zip
|
|
57
|
+
mkdirSync(binDir, { recursive: true });
|
|
58
|
+
extractZip(dest, dataDir);
|
|
59
|
+
rmSync(dest);
|
|
60
|
+
}
|
|
61
|
+
else if (asset.name.includes("pty-win")) {
|
|
62
|
+
console.log(` Downloading ${asset.name}...`);
|
|
63
|
+
const dest = join(dataDir, asset.name);
|
|
64
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
65
|
+
// Remove old node_modules before extracting new pty-win
|
|
66
|
+
const nodeModules = join(ptyWinDir, "node_modules");
|
|
67
|
+
if (existsSync(nodeModules))
|
|
68
|
+
rmSync(nodeModules, { recursive: true });
|
|
69
|
+
await httpsDownload(asset.browser_download_url, dest);
|
|
70
|
+
extractZip(dest, dataDir);
|
|
71
|
+
rmSync(dest);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Save version
|
|
75
|
+
mkdirSync(join(dataDir, "bin"), { recursive: true });
|
|
76
|
+
writeFileSync(versionFile, tag, "utf-8");
|
|
77
|
+
return tag;
|
|
78
|
+
}
|
|
79
|
+
function extractZip(zipPath, destDir) {
|
|
80
|
+
// Use tar on all platforms (Windows 10+ has tar built in)
|
|
81
|
+
try {
|
|
82
|
+
execSync(`tar -xf "${zipPath}" -C "${destDir}"`, { stdio: "pipe" });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Fallback: PowerShell on Windows
|
|
86
|
+
if (process.platform === "win32") {
|
|
87
|
+
execSync(`powershell -NoProfile -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, { stdio: "pipe" });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
execSync(`unzip -qo "${zipPath}" -d "${destDir}"`, { stdio: "pipe" });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { detectPlatform } from "./platform.js";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
/** ~/.fellow-agents/ */
|
|
7
|
+
export const dataDir = join(homedir(), ".fellow-agents");
|
|
8
|
+
/** ~/.fellow-agents/bin/{platform}/ */
|
|
9
|
+
export const binDir = join(dataDir, "bin", detectPlatform());
|
|
10
|
+
/** ~/.fellow-agents/pty-win/ */
|
|
11
|
+
export const ptyWinDir = join(dataDir, "pty-win");
|
|
12
|
+
/** ~/.fellow-agents/pid/ */
|
|
13
|
+
export const pidDir = join(dataDir, "pid");
|
|
14
|
+
/** ~/.fellow-agents/logs/ */
|
|
15
|
+
export const logsDir = join(dataDir, "logs");
|
|
16
|
+
/** ~/.fellow-agents/bin/.version */
|
|
17
|
+
export const versionFile = join(dataDir, "bin", ".version");
|
|
18
|
+
/** templates/ directory shipped with the npm package */
|
|
19
|
+
export const templatesDir = join(__dirname, "..", "..", "templates");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { platform, arch } from "os";
|
|
2
|
+
export function detectPlatform() {
|
|
3
|
+
const os = platform();
|
|
4
|
+
const cpu = arch();
|
|
5
|
+
if (os === "win32")
|
|
6
|
+
return "win-x64";
|
|
7
|
+
if (os === "darwin")
|
|
8
|
+
return cpu === "arm64" ? "osx-arm64" : "osx-x64";
|
|
9
|
+
if (os === "linux")
|
|
10
|
+
return "linux-x64";
|
|
11
|
+
throw new Error(`Unsupported platform: ${os}-${cpu}`);
|
|
12
|
+
}
|
|
13
|
+
export function binarySuffix() {
|
|
14
|
+
return platform() === "win32" ? ".exe" : "";
|
|
15
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import https from "https";
|
|
4
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync, openSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { pidDir, binDir, ptyWinDir, logsDir } from "./paths.js";
|
|
7
|
+
import { binarySuffix } from "./platform.js";
|
|
8
|
+
function writePid(name, pid) {
|
|
9
|
+
mkdirSync(pidDir, { recursive: true });
|
|
10
|
+
writeFileSync(join(pidDir, `${name}.pid`), String(pid), "utf-8");
|
|
11
|
+
}
|
|
12
|
+
function readPid(name) {
|
|
13
|
+
const file = join(pidDir, `${name}.pid`);
|
|
14
|
+
if (!existsSync(file))
|
|
15
|
+
return null;
|
|
16
|
+
const pid = parseInt(readFileSync(file, "utf-8").trim(), 10);
|
|
17
|
+
return isNaN(pid) ? null : pid;
|
|
18
|
+
}
|
|
19
|
+
function removePid(name) {
|
|
20
|
+
const file = join(pidDir, `${name}.pid`);
|
|
21
|
+
if (existsSync(file))
|
|
22
|
+
rmSync(file);
|
|
23
|
+
}
|
|
24
|
+
function openLog(name) {
|
|
25
|
+
mkdirSync(logsDir, { recursive: true });
|
|
26
|
+
return openSync(join(logsDir, `${name}.log`), "w");
|
|
27
|
+
}
|
|
28
|
+
export function logPath(name) {
|
|
29
|
+
return join(logsDir, `${name}.log`);
|
|
30
|
+
}
|
|
31
|
+
function isRunning(pid) {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function startEmcomServer(emcomPort, env) {
|
|
41
|
+
const bin = join(binDir, `emcom-server${binarySuffix()}`);
|
|
42
|
+
const log = openLog("emcom-server");
|
|
43
|
+
const proc = spawn(bin, ["--port", String(emcomPort)], {
|
|
44
|
+
env: { ...env, EMCOM_PORT: String(emcomPort) },
|
|
45
|
+
detached: true,
|
|
46
|
+
stdio: ["ignore", log, log],
|
|
47
|
+
});
|
|
48
|
+
proc.unref();
|
|
49
|
+
writePid("emcom-server", proc.pid);
|
|
50
|
+
return proc.pid;
|
|
51
|
+
}
|
|
52
|
+
export function startPtyWin(port, workspacesDir, emcomUrl, env) {
|
|
53
|
+
const main = join(ptyWinDir, "dist", "index.js");
|
|
54
|
+
const log = openLog("pty-win");
|
|
55
|
+
const proc = spawn("node", [main, "--port", String(port), "--root", workspacesDir, "--emcom", emcomUrl], {
|
|
56
|
+
env,
|
|
57
|
+
detached: true,
|
|
58
|
+
stdio: ["ignore", log, log],
|
|
59
|
+
});
|
|
60
|
+
proc.unref();
|
|
61
|
+
writePid("pty-win", proc.pid);
|
|
62
|
+
return proc.pid;
|
|
63
|
+
}
|
|
64
|
+
export function stopAll() {
|
|
65
|
+
for (const name of ["emcom-server", "pty-win"]) {
|
|
66
|
+
const pid = readPid(name);
|
|
67
|
+
if (pid && isRunning(pid)) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid);
|
|
70
|
+
console.log(` Stopped ${name} (pid ${pid})`);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
console.log(` Could not stop ${name} (pid ${pid})`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(` ${name} not running`);
|
|
78
|
+
}
|
|
79
|
+
removePid(name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function waitForHealth(url, timeoutMs = 30000) {
|
|
83
|
+
const mod = url.startsWith("https") ? https : http;
|
|
84
|
+
const startTime = Date.now();
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const check = () => {
|
|
87
|
+
if (Date.now() - startTime > timeoutMs)
|
|
88
|
+
return resolve(false);
|
|
89
|
+
mod.get(url, (res) => {
|
|
90
|
+
resolve(res.statusCode === 200);
|
|
91
|
+
}).on("error", () => {
|
|
92
|
+
setTimeout(check, 500);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
check();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cpSync, mkdirSync, existsSync, readdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { templatesDir, binDir } from "./paths.js";
|
|
5
|
+
import { binarySuffix } from "./platform.js";
|
|
6
|
+
export function scaffoldWorkspaces(targetDir) {
|
|
7
|
+
const workspacesDir = join(targetDir, "workspaces");
|
|
8
|
+
if (existsSync(workspacesDir) && readdirSync(workspacesDir).length > 0) {
|
|
9
|
+
console.log(" Workspaces already exist — skipping scaffold");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
console.log(" Scaffolding workspaces from templates...");
|
|
13
|
+
mkdirSync(workspacesDir, { recursive: true });
|
|
14
|
+
cpSync(templatesDir, workspacesDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
export function registerAgents(workspacesDir, env) {
|
|
17
|
+
const emcom = join(binDir, `emcom${binarySuffix()}`);
|
|
18
|
+
const dirs = readdirSync(workspacesDir, { withFileTypes: true })
|
|
19
|
+
.filter((d) => d.isDirectory());
|
|
20
|
+
for (const dir of dirs) {
|
|
21
|
+
const idFile = join(workspacesDir, dir.name, "identity.json");
|
|
22
|
+
if (!existsSync(idFile))
|
|
23
|
+
continue;
|
|
24
|
+
try {
|
|
25
|
+
execSync(`"${emcom}" --identity "${idFile}" register --force`, {
|
|
26
|
+
env,
|
|
27
|
+
stdio: "pipe",
|
|
28
|
+
timeout: 10000,
|
|
29
|
+
});
|
|
30
|
+
console.log(` Registered: ${dir.name}`);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
console.log(` Warning: failed to register ${dir.name}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function writeHooks(workspacesDir, ptyWinPort) {
|
|
38
|
+
const dirs = readdirSync(workspacesDir, { withFileTypes: true })
|
|
39
|
+
.filter((d) => d.isDirectory());
|
|
40
|
+
for (const dir of dirs) {
|
|
41
|
+
const claudeDir = join(workspacesDir, dir.name, ".claude");
|
|
42
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
43
|
+
const settings = {
|
|
44
|
+
hooks: {
|
|
45
|
+
Stop: [{ matcher: "", hooks: [{ type: "http", url: `http://127.0.0.1:${ptyWinPort}/api/hook/stop`, timeout: 2 }] }],
|
|
46
|
+
Notification: [{ matcher: "idle_prompt|permission_prompt", hooks: [{ type: "http", url: `http://127.0.0.1:${ptyWinPort}/api/hook/notify`, timeout: 2 }] }],
|
|
47
|
+
UserPromptSubmit: [{ matcher: "", hooks: [{ type: "http", url: `http://127.0.0.1:${ptyWinPort}/api/hook/prompt-submit`, timeout: 2 }] }],
|
|
48
|
+
},
|
|
49
|
+
messageIdleNotifThresholdMs: 5000,
|
|
50
|
+
};
|
|
51
|
+
writeFileSync(join(claudeDir, "settings.local.json"), JSON.stringify(settings, null, 2), "utf-8");
|
|
52
|
+
console.log(` Hooks configured: ${dir.name}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fellow-agents",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Multi-agent system — multiple Claude Code instances collaborating via messaging",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fellow-agents": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"templates/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"agents",
|
|
23
|
+
"multi-agent",
|
|
24
|
+
"terminal",
|
|
25
|
+
"collaboration"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/rajan-chari/fellow-agents"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.6.0",
|
|
34
|
+
"typescript": "^6.0.2"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Coder
|
|
2
|
+
|
|
3
|
+
You write, fix, and improve code based on task descriptions from the coordinator.
|
|
4
|
+
|
|
5
|
+
## On Load
|
|
6
|
+
|
|
7
|
+
1. Register with emcom: `emcom --identity identity.json register 2>/dev/null || true`
|
|
8
|
+
2. Check messages: `emcom --identity identity.json inbox`
|
|
9
|
+
3. Greet the user and check for pending work
|
|
10
|
+
|
|
11
|
+
## Communication
|
|
12
|
+
|
|
13
|
+
- `emcom --identity identity.json send --to coordinator --subject "..." --body "..."` — report results
|
|
14
|
+
- `emcom --identity identity.json inbox` — check for tasks
|
|
15
|
+
- When done, message the coordinator with what you did and relevant file paths
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name": "coder", "description": "Coder agent — writes and fixes code", "server": "http://127.0.0.1:8800"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Coordinator
|
|
2
|
+
|
|
3
|
+
You coordinate a team of AI agents. Break down goals, delegate tasks, collect results.
|
|
4
|
+
|
|
5
|
+
## On Load
|
|
6
|
+
|
|
7
|
+
1. Register with emcom: `emcom --identity identity.json register 2>/dev/null || true`
|
|
8
|
+
2. Check messages: `emcom --identity identity.json inbox`
|
|
9
|
+
3. Ask the user what they'd like the team to work on
|
|
10
|
+
|
|
11
|
+
## Communication
|
|
12
|
+
|
|
13
|
+
- `emcom --identity identity.json send --to <name> --subject "..." --body "..."` — send a message
|
|
14
|
+
- `emcom --identity identity.json inbox` — check messages
|
|
15
|
+
- `emcom who` — see all agents
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
1. User gives you a goal
|
|
20
|
+
2. Delegate to **coder** (implementation) or **reviewer** (analysis)
|
|
21
|
+
3. Collect results via emcom
|
|
22
|
+
4. Report back to user
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name": "coordinator", "description": "Task coordinator — delegates work and collects results", "server": "http://127.0.0.1:8800"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Reviewer
|
|
2
|
+
|
|
3
|
+
You review code for quality, bugs, and improvements.
|
|
4
|
+
|
|
5
|
+
## On Load
|
|
6
|
+
|
|
7
|
+
1. Register with emcom: `emcom --identity identity.json register 2>/dev/null || true`
|
|
8
|
+
2. Check messages: `emcom --identity identity.json inbox`
|
|
9
|
+
3. Greet the user and check for pending reviews
|
|
10
|
+
|
|
11
|
+
## Communication
|
|
12
|
+
|
|
13
|
+
- `emcom --identity identity.json send --to coordinator --subject "..." --body "..."` — report findings
|
|
14
|
+
- `emcom --identity identity.json send --to coder --subject "..." --body "..."` — send feedback directly
|
|
15
|
+
- `emcom --identity identity.json inbox` — check for review requests
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name": "reviewer", "description": "Reviewer agent — reviews code for quality and correctness", "server": "http://127.0.0.1:8800"}
|