fellow-agents 0.0.14 → 0.0.16
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/commands/start.js +23 -10
- package/dist/commands/uninstall.js +11 -0
- package/dist/lib/paths.js +2 -0
- package/dist/lib/skills.js +135 -0
- package/package.json +3 -2
- package/skills/emcom/SKILL.md +191 -0
package/dist/commands/start.js
CHANGED
|
@@ -6,6 +6,7 @@ import { binDir, ptyWinDir, logsDir } from "../lib/paths.js";
|
|
|
6
6
|
import { downloadBinaries } from "../lib/download.js";
|
|
7
7
|
import { startEmcomServer, startPtyWin, stopAll, logPath } from "../lib/services.js";
|
|
8
8
|
import { scaffoldWorkspaces, registerAgents, writeHooks } from "../lib/workspaces.js";
|
|
9
|
+
import { installSkills } from "../lib/skills.js";
|
|
9
10
|
import { binarySuffix } from "../lib/platform.js";
|
|
10
11
|
// Minimal engines.node range check — handles ">=N", "<N", and combinations.
|
|
11
12
|
function nodeInRange(version, range) {
|
|
@@ -32,7 +33,7 @@ export async function start(opts) {
|
|
|
32
33
|
const workspacesDir = join(workDir, "workspaces");
|
|
33
34
|
const emcomUrl = `http://127.0.0.1:${opts.emcomPort}`;
|
|
34
35
|
// 1. Prerequisites
|
|
35
|
-
console.log("[1/
|
|
36
|
+
console.log("[1/8] Checking prerequisites...");
|
|
36
37
|
const nodeVer = process.versions.node;
|
|
37
38
|
const nodeMajor = parseInt(nodeVer.split(".")[0], 10);
|
|
38
39
|
if (nodeMajor < 18) {
|
|
@@ -48,7 +49,7 @@ export async function start(opts) {
|
|
|
48
49
|
console.log(" Claude Code not found (optional — install from https://claude.ai/code)");
|
|
49
50
|
}
|
|
50
51
|
// 2. Download binaries
|
|
51
|
-
console.log("[2/
|
|
52
|
+
console.log("[2/8] Downloading binaries...");
|
|
52
53
|
try {
|
|
53
54
|
await downloadBinaries(opts.update);
|
|
54
55
|
}
|
|
@@ -61,7 +62,7 @@ export async function start(opts) {
|
|
|
61
62
|
console.log(" Using cached binaries");
|
|
62
63
|
}
|
|
63
64
|
// 3. Install pty-win dependencies
|
|
64
|
-
console.log("[3/
|
|
65
|
+
console.log("[3/8] Installing pty-win...");
|
|
65
66
|
const ptyPkgPath = join(ptyWinDir, "package.json");
|
|
66
67
|
if (!existsSync(ptyPkgPath)) {
|
|
67
68
|
console.error(" pty-win not found — download may have failed");
|
|
@@ -91,12 +92,24 @@ export async function start(opts) {
|
|
|
91
92
|
}
|
|
92
93
|
console.log(" pty-win ready");
|
|
93
94
|
// 4. Scaffold workspaces
|
|
94
|
-
console.log("[4/
|
|
95
|
+
console.log("[4/8] Scaffolding workspaces...");
|
|
95
96
|
scaffoldWorkspaces(workDir);
|
|
97
|
+
// 5. Install AI skills (SKILL.md files) to known CLI paths
|
|
98
|
+
console.log("[5/8] Installing skills...");
|
|
99
|
+
const skillResult = installSkills();
|
|
100
|
+
if (skillResult.written.length > 0) {
|
|
101
|
+
console.log(` Installed ${skillResult.written.length} skill file(s)`);
|
|
102
|
+
}
|
|
103
|
+
if (skillResult.skipped.length > 0) {
|
|
104
|
+
console.log(` Preserved ${skillResult.skipped.length} existing skill file(s) — delete to refresh`);
|
|
105
|
+
}
|
|
106
|
+
if (skillResult.written.length === 0 && skillResult.skipped.length === 0) {
|
|
107
|
+
console.log(" No skills bundled");
|
|
108
|
+
}
|
|
96
109
|
// PATH trick: prepend bin dir so agents find emcom/tracker
|
|
97
110
|
const env = { ...process.env, PATH: `${binDir}${process.platform === "win32" ? ";" : ":"}${process.env.PATH}` };
|
|
98
|
-
//
|
|
99
|
-
console.log("[
|
|
111
|
+
// 6. Start emcom-server
|
|
112
|
+
console.log("[6/8] Starting emcom-server...");
|
|
100
113
|
const emcomPid = startEmcomServer(opts.emcomPort, env);
|
|
101
114
|
console.log(` emcom-server started (pid ${emcomPid})`);
|
|
102
115
|
// Wait for health
|
|
@@ -126,12 +139,12 @@ export async function start(opts) {
|
|
|
126
139
|
console.error(` Warning: emcom-server health check failed — it may not be running`);
|
|
127
140
|
console.error(` Check logs: ${logPath("emcom-server")}`);
|
|
128
141
|
}
|
|
129
|
-
//
|
|
130
|
-
console.log("[
|
|
142
|
+
// 7. Register agents
|
|
143
|
+
console.log("[7/8] Registering agents + configuring hooks...");
|
|
131
144
|
registerAgents(workspacesDir, env);
|
|
132
145
|
writeHooks(workspacesDir, opts.port);
|
|
133
|
-
//
|
|
134
|
-
console.log("[
|
|
146
|
+
// 8. Start pty-win
|
|
147
|
+
console.log("[8/8] Starting pty-win...");
|
|
135
148
|
const ptyPid = startPtyWin(opts.port, workspacesDir, emcomUrl, env);
|
|
136
149
|
console.log(` pty-win started (pid ${ptyPid})`);
|
|
137
150
|
// Open browser
|
|
@@ -2,6 +2,7 @@ import { existsSync, rmSync, statSync, readdirSync } from "fs";
|
|
|
2
2
|
import { join, resolve } from "path";
|
|
3
3
|
import { dataDir } from "../lib/paths.js";
|
|
4
4
|
import { stopAll } from "../lib/services.js";
|
|
5
|
+
import { uninstallSkills } from "../lib/skills.js";
|
|
5
6
|
function dirSize(path) {
|
|
6
7
|
if (!existsSync(path))
|
|
7
8
|
return 0;
|
|
@@ -66,6 +67,7 @@ export function uninstall(opts) {
|
|
|
66
67
|
for (const t of targets) {
|
|
67
68
|
console.log(` ${t.path} (${formatBytes(t.size)})`);
|
|
68
69
|
}
|
|
70
|
+
console.log(` Skill files installed by fellow-agents (customized files preserved)`);
|
|
69
71
|
console.log("");
|
|
70
72
|
console.log(` Total: ${formatBytes(totalSize)}`);
|
|
71
73
|
console.log("");
|
|
@@ -81,6 +83,15 @@ export function uninstall(opts) {
|
|
|
81
83
|
console.log(" Stopping services...");
|
|
82
84
|
stopAll();
|
|
83
85
|
console.log("");
|
|
86
|
+
// Remove skills we installed (only the ones that match the shipped bytes —
|
|
87
|
+
// user-customized files are preserved).
|
|
88
|
+
const skillResult = uninstallSkills();
|
|
89
|
+
if (skillResult.removed.length > 0) {
|
|
90
|
+
console.log(` Removed ${skillResult.removed.length} skill file(s)`);
|
|
91
|
+
}
|
|
92
|
+
if (skillResult.preserved.length > 0) {
|
|
93
|
+
console.log(` Preserved ${skillResult.preserved.length} customized skill file(s)`);
|
|
94
|
+
}
|
|
84
95
|
for (const t of targets) {
|
|
85
96
|
try {
|
|
86
97
|
rmSync(t.path, { recursive: true, force: true });
|
package/dist/lib/paths.js
CHANGED
|
@@ -17,3 +17,5 @@ export const logsDir = join(dataDir, "logs");
|
|
|
17
17
|
export const versionFile = join(dataDir, "bin", ".version");
|
|
18
18
|
/** templates/ directory shipped with the npm package */
|
|
19
19
|
export const templatesDir = join(__dirname, "..", "..", "templates");
|
|
20
|
+
/** skills/ directory shipped with the npm package */
|
|
21
|
+
export const skillsDir = join(__dirname, "..", "..", "skills");
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, readdirSync, mkdirSync, copyFileSync, readFileSync, statSync, rmSync, rmdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { skillsDir } from "./paths.js";
|
|
5
|
+
// Three target paths per agentskills.io convention — installed CLIs vary; we write to all three
|
|
6
|
+
// so the skill works regardless of which AI CLI the user is using.
|
|
7
|
+
const targetRoots = [
|
|
8
|
+
join(homedir(), ".claude", "skills"), // Claude Code
|
|
9
|
+
join(homedir(), ".copilot", "skills"), // GitHub Copilot CLI
|
|
10
|
+
join(homedir(), ".agents", "skills"), // pi + cross-tool universal location
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* Copy bundled skills to all known target paths.
|
|
14
|
+
*
|
|
15
|
+
* Strategy: write if absent. Never overwrite existing files (treated as
|
|
16
|
+
* user-customized). To force a refresh, the user can delete the target file
|
|
17
|
+
* and re-run fellow-agents.
|
|
18
|
+
*/
|
|
19
|
+
export function installSkills() {
|
|
20
|
+
const result = { written: [], skipped: [] };
|
|
21
|
+
if (!existsSync(skillsDir))
|
|
22
|
+
return result;
|
|
23
|
+
const skillNames = readdirSync(skillsDir).filter((name) => {
|
|
24
|
+
try {
|
|
25
|
+
return statSync(join(skillsDir, name)).isDirectory();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
for (const skillName of skillNames) {
|
|
32
|
+
const sourceSkillDir = join(skillsDir, skillName);
|
|
33
|
+
const skillFiles = walkSkillFiles(sourceSkillDir);
|
|
34
|
+
for (const relPath of skillFiles) {
|
|
35
|
+
const sourceFile = join(sourceSkillDir, relPath);
|
|
36
|
+
for (const root of targetRoots) {
|
|
37
|
+
const targetFile = join(root, skillName, relPath);
|
|
38
|
+
if (existsSync(targetFile)) {
|
|
39
|
+
// User-customized or already-installed — never overwrite
|
|
40
|
+
result.skipped.push(targetFile);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
mkdirSync(join(root, skillName, ...relPath.split(/[\\/]/).slice(0, -1)), { recursive: true });
|
|
44
|
+
copyFileSync(sourceFile, targetFile);
|
|
45
|
+
result.written.push(targetFile);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Remove skill files we installed, if the user hasn't modified them.
|
|
53
|
+
*
|
|
54
|
+
* Compares each target file byte-for-byte against the bundled version.
|
|
55
|
+
* Matching → safe to delete (we wrote it, nothing changed). Differing →
|
|
56
|
+
* preserve (user customized or it was already there before we showed up).
|
|
57
|
+
*
|
|
58
|
+
* After file removal, attempts to clean up empty skill directories and
|
|
59
|
+
* empty target root directories (e.g., ~/.copilot/skills/ if user doesn't
|
|
60
|
+
* have Copilot installed).
|
|
61
|
+
*/
|
|
62
|
+
export function uninstallSkills() {
|
|
63
|
+
const result = { removed: [], preserved: [] };
|
|
64
|
+
if (!existsSync(skillsDir))
|
|
65
|
+
return result;
|
|
66
|
+
const skillNames = readdirSync(skillsDir).filter((name) => {
|
|
67
|
+
try {
|
|
68
|
+
return statSync(join(skillsDir, name)).isDirectory();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
for (const skillName of skillNames) {
|
|
75
|
+
const sourceSkillDir = join(skillsDir, skillName);
|
|
76
|
+
const skillFiles = walkSkillFiles(sourceSkillDir);
|
|
77
|
+
for (const root of targetRoots) {
|
|
78
|
+
const targetSkillDir = join(root, skillName);
|
|
79
|
+
if (!existsSync(targetSkillDir))
|
|
80
|
+
continue;
|
|
81
|
+
for (const relPath of skillFiles) {
|
|
82
|
+
const sourceFile = join(sourceSkillDir, relPath);
|
|
83
|
+
const targetFile = join(targetSkillDir, relPath);
|
|
84
|
+
if (!existsSync(targetFile))
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
const sourceBytes = readFileSync(sourceFile);
|
|
88
|
+
const targetBytes = readFileSync(targetFile);
|
|
89
|
+
if (Buffer.compare(sourceBytes, targetBytes) === 0) {
|
|
90
|
+
rmSync(targetFile);
|
|
91
|
+
result.removed.push(targetFile);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
result.preserved.push(targetFile);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Can't read either file — skip, don't risk data loss
|
|
99
|
+
result.preserved.push(targetFile);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Try to remove empty skill dir (best effort — won't remove if user has
|
|
103
|
+
// their own files in there or any preserved files remain)
|
|
104
|
+
tryRemoveIfEmpty(targetSkillDir);
|
|
105
|
+
tryRemoveIfEmpty(root);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
function tryRemoveIfEmpty(dir) {
|
|
111
|
+
try {
|
|
112
|
+
if (existsSync(dir) && readdirSync(dir).length === 0) {
|
|
113
|
+
rmdirSync(dir);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
}
|
|
118
|
+
function walkSkillFiles(dir, prefix = "") {
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const entry of readdirSync(dir)) {
|
|
121
|
+
const full = join(dir, entry);
|
|
122
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
123
|
+
try {
|
|
124
|
+
const stat = statSync(full);
|
|
125
|
+
if (stat.isDirectory()) {
|
|
126
|
+
results.push(...walkSkillFiles(full, rel));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
results.push(rel);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
}
|
|
134
|
+
return results;
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fellow-agents",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "Multi-agent system — multiple Claude Code instances collaborating via messaging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist/",
|
|
16
|
-
"templates/"
|
|
16
|
+
"templates/",
|
|
17
|
+
"skills/"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
20
|
"build": "tsc",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: emcom
|
|
3
|
+
description: |
|
|
4
|
+
Use this skill when the user wants to interact with the emcom email system,
|
|
5
|
+
or when the agent itself needs to participate in agent-to-agent messaging.
|
|
6
|
+
Triggers: check email, send message, who's online, register, inbox, reply,
|
|
7
|
+
tag emails, search messages, list threads, any email-related request,
|
|
8
|
+
or arrival of an emcom auto-inject prompt.
|
|
9
|
+
tools:
|
|
10
|
+
- Bash
|
|
11
|
+
- Read
|
|
12
|
+
- Write
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# emcom Skill
|
|
16
|
+
|
|
17
|
+
emcom is an email-metaphor messaging system for AI-agent communication. Multiple agents (Claude Code, GitHub Copilot CLI, pi, etc.) exchange messages using email semantics: threads, tags, sent/received, search. It is **asynchronous, persistent, and crash-safe** — messages survive session restarts and unhandled messages remain visible until explicitly processed.
|
|
18
|
+
|
|
19
|
+
You are the agent — you participate in the team using these commands. This skill describes both the protocol semantics (how the team uses emcom) and the CLI mechanics (how to type the commands correctly).
|
|
20
|
+
|
|
21
|
+
## Overview & mental model
|
|
22
|
+
|
|
23
|
+
- **Mailboxes per identity**: each agent has a `name` and an `identity.json` in its CWD. Messages addressed to you appear in your inbox.
|
|
24
|
+
- **Threads**: replies stay grouped under a `thread_id`; new topics start a new thread.
|
|
25
|
+
- **Tags**: workflow state (`unread`, `pending`, `handled`) plus freeform custom tags.
|
|
26
|
+
- **Server**: a single emcom-server (port 8800) holds all messages. Each agent's CLI talks to it via HTTP.
|
|
27
|
+
- **Multi-agent ecosystem**: an agent never operates alone. Other agents (named in `emcom who`) read what you send, react to what you tag, depend on what you handle promptly.
|
|
28
|
+
|
|
29
|
+
## Identity & registration
|
|
30
|
+
|
|
31
|
+
Each working directory has its own `identity.json` (name, server URL, optional description). Commands automatically use the identity in the current directory's `identity.json`. Do NOT pass `--identity` to cross directories; instead, `cd` to the directory or run the command from that working directory.
|
|
32
|
+
|
|
33
|
+
Global flags (`--identity`, `-i`, `--server`) go **before** the subcommand:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
emcom --identity path/to/id.json inbox
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If no identity exists yet and a command fails with "Missing X-Emcom-Name header":
|
|
40
|
+
|
|
41
|
+
1. `emcom names` — lists available names from the pool. Pick one yourself. Do NOT ask the user. Do NOT invent a name not in the list.
|
|
42
|
+
2. `emcom register --name <CHOSEN_NAME> [--description "<role>"]`
|
|
43
|
+
3. Retry the original command.
|
|
44
|
+
|
|
45
|
+
The `--description` is a short tag visible in `emcom who` — useful for new agents to declare their role (e.g. "Build coordinator for fellow-agents repo").
|
|
46
|
+
|
|
47
|
+
## Message lifecycle
|
|
48
|
+
|
|
49
|
+
The canonical flow:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
emcom inbox # show unhandled messages
|
|
53
|
+
emcom read <id> # read body (auto-tags: pending)
|
|
54
|
+
... evaluate, optionally reply or act ...
|
|
55
|
+
emcom tag <id> handled # done — message exits inbox
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Tag state machine:
|
|
59
|
+
|
|
60
|
+
| Tag | Set by | Cleared by | Inbox effect |
|
|
61
|
+
|-----|--------|------------|--------------|
|
|
62
|
+
| `unread` | System on delivery | System on first `read` | Visible |
|
|
63
|
+
| `pending` | System on first `read` | System when `handled` is set | Visible |
|
|
64
|
+
| `handled` | Agent (manually) | Agent (via `untag`) | **Hidden** |
|
|
65
|
+
| custom | Agent | Agent | Visible (unless also `handled`) |
|
|
66
|
+
|
|
67
|
+
Inbox shows everything not tagged `handled`. `inbox --all` shows handled too. There is no "delete" — `handled` is the closure signal.
|
|
68
|
+
|
|
69
|
+
**Crash safety**: if your session dies between reading and tagging `handled`, the message reappears in inbox on next `inbox`. This is by design — don't tag handled until the work is actually done.
|
|
70
|
+
|
|
71
|
+
## Triage rules
|
|
72
|
+
|
|
73
|
+
After reading a message, classify it:
|
|
74
|
+
|
|
75
|
+
- **Actionable + recent**: do the work, reply if appropriate, tag handled.
|
|
76
|
+
- **Informational**: tag handled. No reply needed.
|
|
77
|
+
- **Question or FYI**: short reply acknowledging, then tag handled.
|
|
78
|
+
- **Stale (>2 hours old) and action-oriented**: acknowledge-don't-act. The world has moved on; running the requested action now may step on completed work. Reply "saw this, was stale, didn't act" if the sender needs closure, then tag handled.
|
|
79
|
+
- **Targeted at someone else** (you were CC'd): tag handled. Reply only if you have substantive input.
|
|
80
|
+
- **Stop the loop**: if a thread is going in circles with no progress, send one summarizing message ("here's the state, I'm done") and stop. Don't let courtesy replies extend dead threads.
|
|
81
|
+
|
|
82
|
+
Read multiple messages, batch your work, tag handled in any order — the only invariant is **tag handled after the work is done, not before**.
|
|
83
|
+
|
|
84
|
+
## Threading
|
|
85
|
+
|
|
86
|
+
Two ways to send:
|
|
87
|
+
|
|
88
|
+
- **`emcom send`**: starts a new thread. Use for new topics, unrelated conversations, or when threading would muddle the search.
|
|
89
|
+
- **`emcom reply <id>`**: continues the thread of message `<id>`. Use when the message is logically a response to a prior conversation. Threading helps later searches (`emcom thread <id>` recovers the whole exchange).
|
|
90
|
+
|
|
91
|
+
Reply hygiene:
|
|
92
|
+
- Keep replies in-thread unless the topic genuinely shifts.
|
|
93
|
+
- If you're acknowledging multiple parallel things from one agent, prefer one reply per thread rather than batching into one.
|
|
94
|
+
- Don't reply just to say "ok" — tag handled instead. Replies create work for the other agent (their inbox grows).
|
|
95
|
+
|
|
96
|
+
## Context recovery
|
|
97
|
+
|
|
98
|
+
When a session starts fresh and you find messages in your inbox you don't recognize:
|
|
99
|
+
|
|
100
|
+
1. **Read the message** — `emcom read <id>` shows full body and any tags.
|
|
101
|
+
2. **Recover thread context** — if the message is part of a thread, `emcom thread <thread_id>` shows the full exchange. The thread_id is in the read output. Read the full thread before replying or acting, especially if the topic involves multi-step work or coordination.
|
|
102
|
+
3. **Check briefing files** — if your workspace has a `briefing.md` or `notes.md`, your prior-session self likely captured relevant context there. Read those before responding to async work.
|
|
103
|
+
4. **Search by sender** — `emcom search --from <name>` finds all prior exchanges with one agent.
|
|
104
|
+
5. **Search by subject** — `emcom search --subject <text>` finds related conversations.
|
|
105
|
+
|
|
106
|
+
Don't reply blind to in-flight work. A short delay to read the thread first prevents the "two agents talking past each other" failure mode.
|
|
107
|
+
|
|
108
|
+
## Running commands
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
emcom <subcommand> [args]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
If `emcom` is not in PATH, the binary lives at one of:
|
|
115
|
+
- `~/.claude/skills/emcom/bin/emcom` (Claude Code skill install)
|
|
116
|
+
- `~/.copilot/skills/emcom/bin/emcom` (GitHub Copilot skill install)
|
|
117
|
+
- `~/.agents/skills/emcom/bin/emcom` (pi / universal skill install)
|
|
118
|
+
- Or whatever PATH was set up by `npm install -g fellow-agents`
|
|
119
|
+
|
|
120
|
+
**Permission-friendly invocation** (matters in some CLIs that gate command execution):
|
|
121
|
+
|
|
122
|
+
- **One command per Bash call**. Do NOT chain with `&&`, `;`, or `||`.
|
|
123
|
+
- Do NOT assign the binary path to a variable. Use the bare command or literal path.
|
|
124
|
+
- **Independent** operations (e.g. reading 3 emails): run as parallel Bash tool calls.
|
|
125
|
+
- **Sequential** operations (e.g. register then inbox): separate sequential Bash calls.
|
|
126
|
+
|
|
127
|
+
## Error recovery
|
|
128
|
+
|
|
129
|
+
**Auth error** ("Missing X-Emcom-Name header"):
|
|
130
|
+
1. `emcom names` — read the output, pick a name yourself.
|
|
131
|
+
2. `emcom register --name <NAME>`
|
|
132
|
+
3. Retry original command.
|
|
133
|
+
|
|
134
|
+
**Connection error** ("Connection refused", ECONNREFUSED):
|
|
135
|
+
1. Start server: `emcom-server &` (or full path if not in PATH).
|
|
136
|
+
2. `sleep 2`
|
|
137
|
+
3. Retry.
|
|
138
|
+
|
|
139
|
+
**Wrong identity** (you sent as the wrong agent): no automatic fix. Send a follow-up message from the correct identity explaining the mistake, tag the original handled, move on.
|
|
140
|
+
|
|
141
|
+
**Stale local state** (you see "already registered" but server says otherwise): `emcom register --force --name <NAME>`. Use sparingly; usually the server is right.
|
|
142
|
+
|
|
143
|
+
## Custom tag conventions
|
|
144
|
+
|
|
145
|
+
The team uses these custom tags by convention (your team may differ — check briefing.md or ask):
|
|
146
|
+
|
|
147
|
+
| Tag | Meaning |
|
|
148
|
+
|-----|---------|
|
|
149
|
+
| `urgent` | Needs response within minutes, not hours |
|
|
150
|
+
| `blocker` | Blocking another agent's progress; resolve first |
|
|
151
|
+
| `fyi` | No reply needed; informational only |
|
|
152
|
+
| `decided` | Decision recorded, future references can grep this |
|
|
153
|
+
| `parked` | Real but deferred; revisit when conditions change |
|
|
154
|
+
|
|
155
|
+
You may invent additional custom tags freely. Tags are cheap and grep-friendly.
|
|
156
|
+
|
|
157
|
+
## Command reference
|
|
158
|
+
|
|
159
|
+
| User intent | Command |
|
|
160
|
+
|-------------|---------|
|
|
161
|
+
| "register", "join emcom" | `emcom register [--name NAME] [--description DESC] [--force]` |
|
|
162
|
+
| "unregister", "leave" | `emcom unregister` |
|
|
163
|
+
| "who's online", "who's here" | `emcom who` |
|
|
164
|
+
| "update my description" | `emcom update --description DESC` |
|
|
165
|
+
| "check email", "inbox" | `emcom inbox [--all]` |
|
|
166
|
+
| "read email X" | `emcom read ID [--tag TAG...]` |
|
|
167
|
+
| "send email to X" | `emcom send --to NAME [--cc NAME] --subject SUBJ --body BODY` |
|
|
168
|
+
| "reply to X" | `emcom reply ID --body BODY` |
|
|
169
|
+
| "show thread" | `emcom thread THREAD_ID` |
|
|
170
|
+
| "list threads" | `emcom threads` |
|
|
171
|
+
| "sent emails" | `emcom sent` |
|
|
172
|
+
| "all emails", "everything" | `emcom all` |
|
|
173
|
+
| "tag email X as Y" | `emcom tag ID TAG [TAG...]` |
|
|
174
|
+
| "remove tag Y from X" | `emcom untag ID TAG` |
|
|
175
|
+
| "find emails tagged Y" | `emcom tagged TAG` |
|
|
176
|
+
| "search for X" | `emcom search [--from NAME] [--to NAME] [--subject TEXT] [--tag TAG] [--body TEXT]` |
|
|
177
|
+
| "list available names" | `emcom names` |
|
|
178
|
+
| "add names to pool" | `emcom names --add NAME [NAME...]` |
|
|
179
|
+
| "purge", "clean out", "reset" | `emcom purge` |
|
|
180
|
+
|
|
181
|
+
## Notes
|
|
182
|
+
|
|
183
|
+
- Short IDs (first 8 hex chars) work everywhere an ID is accepted.
|
|
184
|
+
- Server: port 8800 (override via `EMCOM_PORT`). Data in `~/.emcom/`.
|
|
185
|
+
- Always use `127.0.0.1` not `localhost` (avoids IPv6 DNS resolution penalty on Windows).
|
|
186
|
+
- `emcom all` shows unified sent+received view with `>>` (sent) / `<<` (received) markers.
|
|
187
|
+
- Backtick-containing bodies via `--body "..."` get shell-expanded — single-quote the body when it contains backticks or `$`.
|
|
188
|
+
|
|
189
|
+
## Output
|
|
190
|
+
|
|
191
|
+
Present CLI output naturally — the formatted tables are designed to be readable as-is. For individual emails, show the full header + body. For lists, show as-returned (don't reformat).
|