fellow-agents 0.0.14 → 0.0.18

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.
@@ -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/7] Checking prerequisites...");
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/7] Downloading binaries...");
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/7] Installing pty-win...");
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,27 @@ export async function start(opts) {
91
92
  }
92
93
  console.log(" pty-win ready");
93
94
  // 4. Scaffold workspaces
94
- console.log("[4/7] Scaffolding workspaces...");
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.refreshed.length > 0) {
104
+ console.log(` Refreshed ${skillResult.refreshed.length} skill file(s) to latest`);
105
+ }
106
+ if (skillResult.skipped.length > 0) {
107
+ console.log(` Preserved ${skillResult.skipped.length} existing skill file(s) — customized or unowned`);
108
+ }
109
+ if (skillResult.written.length === 0 && skillResult.refreshed.length === 0 && skillResult.skipped.length === 0) {
110
+ console.log(" No skills bundled");
111
+ }
96
112
  // PATH trick: prepend bin dir so agents find emcom/tracker
97
113
  const env = { ...process.env, PATH: `${binDir}${process.platform === "win32" ? ";" : ":"}${process.env.PATH}` };
98
- // 5. Start emcom-server
99
- console.log("[5/7] Starting emcom-server...");
114
+ // 6. Start emcom-server
115
+ console.log("[6/8] Starting emcom-server...");
100
116
  const emcomPid = startEmcomServer(opts.emcomPort, env);
101
117
  console.log(` emcom-server started (pid ${emcomPid})`);
102
118
  // Wait for health
@@ -126,12 +142,12 @@ export async function start(opts) {
126
142
  console.error(` Warning: emcom-server health check failed — it may not be running`);
127
143
  console.error(` Check logs: ${logPath("emcom-server")}`);
128
144
  }
129
- // 6. Register agents
130
- console.log("[6/7] Registering agents + configuring hooks...");
145
+ // 7. Register agents
146
+ console.log("[7/8] Registering agents + configuring hooks...");
131
147
  registerAgents(workspacesDir, env);
132
148
  writeHooks(workspacesDir, opts.port);
133
- // 7. Start pty-win
134
- console.log("[7/7] Starting pty-win...");
149
+ // 8. Start pty-win
150
+ console.log("[8/8] Starting pty-win...");
135
151
  const ptyPid = startPtyWin(opts.port, workspacesDir, emcomUrl, env);
136
152
  console.log(` pty-win started (pid ${ptyPid})`);
137
153
  // 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,179 @@
1
+ import { existsSync, readdirSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, statSync, rmSync, rmdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { createHash } from "crypto";
5
+ import { skillsDir } from "./paths.js";
6
+ // Three target paths per agentskills.io convention — installed CLIs vary; we write to all three
7
+ // so the skill works regardless of which AI CLI the user is using.
8
+ const targetRoots = [
9
+ join(homedir(), ".claude", "skills"), // Claude Code
10
+ join(homedir(), ".copilot", "skills"), // GitHub Copilot CLI
11
+ join(homedir(), ".agents", "skills"), // pi + cross-tool universal location
12
+ ];
13
+ // Sidecar suffix — written next to each shipped file, contains the SHA-256 of
14
+ // the content we shipped. Used to detect "we wrote this, user hasn't touched"
15
+ // vs "user customized" on later installs and uninstall.
16
+ const SIDECAR_SUFFIX = ".fellow-agents-shipped";
17
+ /**
18
+ * Copy bundled skills to all known target paths.
19
+ *
20
+ * Update semantics (v0.0.18+):
21
+ * - Target absent: write file + sidecar.
22
+ * - Target has sidecar AND target SHA matches sidecar: we own it, user hasn't
23
+ * modified — safe to refresh with new content. Write file + update sidecar.
24
+ * - Target has sidecar AND target SHA differs from sidecar: user edited our
25
+ * shipped file. Preserve, don't touch sidecar.
26
+ * - Target exists with NO sidecar: pre-existing file (pre-v0.0.18 install, or
27
+ * user-placed). Preserve — we can't prove we own it.
28
+ */
29
+ export function installSkills() {
30
+ const result = { written: [], refreshed: [], skipped: [] };
31
+ if (!existsSync(skillsDir))
32
+ return result;
33
+ const skillNames = readdirSync(skillsDir).filter((name) => {
34
+ try {
35
+ return statSync(join(skillsDir, name)).isDirectory();
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ });
41
+ for (const skillName of skillNames) {
42
+ const sourceSkillDir = join(skillsDir, skillName);
43
+ const skillFiles = walkSkillFiles(sourceSkillDir);
44
+ for (const relPath of skillFiles) {
45
+ const sourceFile = join(sourceSkillDir, relPath);
46
+ const sourceSha = sha256File(sourceFile);
47
+ for (const root of targetRoots) {
48
+ const targetFile = join(root, skillName, relPath);
49
+ const sidecarFile = targetFile + SIDECAR_SUFFIX;
50
+ if (!existsSync(targetFile)) {
51
+ // First-time install
52
+ mkdirSync(join(root, skillName, ...relPath.split(/[\\/]/).slice(0, -1)), { recursive: true });
53
+ copyFileSync(sourceFile, targetFile);
54
+ writeFileSync(sidecarFile, sourceSha, "utf-8");
55
+ result.written.push(targetFile);
56
+ continue;
57
+ }
58
+ // Target exists — decide based on sidecar
59
+ if (!existsSync(sidecarFile)) {
60
+ // No sidecar → we can't prove we own this file, preserve
61
+ result.skipped.push(targetFile);
62
+ continue;
63
+ }
64
+ const recordedSha = readFileSync(sidecarFile, "utf-8").trim();
65
+ const currentSha = sha256File(targetFile);
66
+ if (currentSha === recordedSha) {
67
+ // Target matches what we previously shipped → user hasn't touched → safe refresh
68
+ if (currentSha === sourceSha) {
69
+ // Same shipped version, nothing to do
70
+ result.skipped.push(targetFile);
71
+ }
72
+ else {
73
+ copyFileSync(sourceFile, targetFile);
74
+ writeFileSync(sidecarFile, sourceSha, "utf-8");
75
+ result.refreshed.push(targetFile);
76
+ }
77
+ }
78
+ else {
79
+ // User edited the file after we shipped it — preserve
80
+ result.skipped.push(targetFile);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+ /**
88
+ * Remove skill files we installed, if the user hasn't modified them.
89
+ *
90
+ * Sidecar-based ownership detection (v0.0.18+):
91
+ * - Target has sidecar AND target SHA matches sidecar → we own it, user
92
+ * hasn't touched → delete file + sidecar.
93
+ * - Target has sidecar AND target SHA differs → user customized → preserve
94
+ * (file and sidecar).
95
+ * - Target has no sidecar → we can't prove we own it → preserve.
96
+ *
97
+ * After file removal, attempts to clean up empty skill directories and
98
+ * empty target root directories.
99
+ */
100
+ export function uninstallSkills() {
101
+ const result = { removed: [], preserved: [] };
102
+ if (!existsSync(skillsDir))
103
+ return result;
104
+ const skillNames = readdirSync(skillsDir).filter((name) => {
105
+ try {
106
+ return statSync(join(skillsDir, name)).isDirectory();
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ });
112
+ for (const skillName of skillNames) {
113
+ const sourceSkillDir = join(skillsDir, skillName);
114
+ const skillFiles = walkSkillFiles(sourceSkillDir);
115
+ for (const root of targetRoots) {
116
+ const targetSkillDir = join(root, skillName);
117
+ if (!existsSync(targetSkillDir))
118
+ continue;
119
+ for (const relPath of skillFiles) {
120
+ const targetFile = join(targetSkillDir, relPath);
121
+ const sidecarFile = targetFile + SIDECAR_SUFFIX;
122
+ if (!existsSync(targetFile))
123
+ continue;
124
+ if (!existsSync(sidecarFile)) {
125
+ // No sidecar — can't prove ownership, preserve
126
+ result.preserved.push(targetFile);
127
+ continue;
128
+ }
129
+ try {
130
+ const recordedSha = readFileSync(sidecarFile, "utf-8").trim();
131
+ const currentSha = sha256File(targetFile);
132
+ if (currentSha === recordedSha) {
133
+ rmSync(targetFile);
134
+ rmSync(sidecarFile);
135
+ result.removed.push(targetFile);
136
+ }
137
+ else {
138
+ result.preserved.push(targetFile);
139
+ }
140
+ }
141
+ catch {
142
+ result.preserved.push(targetFile);
143
+ }
144
+ }
145
+ tryRemoveIfEmpty(targetSkillDir);
146
+ tryRemoveIfEmpty(root);
147
+ }
148
+ }
149
+ return result;
150
+ }
151
+ function sha256File(path) {
152
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
153
+ }
154
+ function tryRemoveIfEmpty(dir) {
155
+ try {
156
+ if (existsSync(dir) && readdirSync(dir).length === 0) {
157
+ rmdirSync(dir);
158
+ }
159
+ }
160
+ catch { }
161
+ }
162
+ function walkSkillFiles(dir, prefix = "") {
163
+ const results = [];
164
+ for (const entry of readdirSync(dir)) {
165
+ const full = join(dir, entry);
166
+ const rel = prefix ? `${prefix}/${entry}` : entry;
167
+ try {
168
+ const stat = statSync(full);
169
+ if (stat.isDirectory()) {
170
+ results.push(...walkSkillFiles(full, rel));
171
+ }
172
+ else {
173
+ results.push(rel);
174
+ }
175
+ }
176
+ catch { }
177
+ }
178
+ return results;
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fellow-agents",
3
- "version": "0.0.14",
3
+ "version": "0.0.18",
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,189 @@
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
+ `emcom` is on PATH when fellow-agents has been installed (`npm install -g fellow-agents`) — the npm-created shim wraps `~/.fellow-agents/bin/<platform>/emcom`. **Use the bare command. Do not prepend any skills-directory path.**
115
+
116
+ If a CLI environment can't find `emcom` on PATH, that means fellow-agents isn't installed (or its bin shim hasn't been picked up by the shell). Tell the user to run `npm install -g fellow-agents` rather than guessing at a skill-bundled path — fellow-agents does not ship binaries inside `~/.claude/skills/`, `~/.copilot/skills/`, or `~/.agents/skills/`.
117
+
118
+ **Permission-friendly invocation** (matters in some CLIs that gate command execution):
119
+
120
+ - **One command per Bash call**. Do NOT chain with `&&`, `;`, or `||`.
121
+ - Do NOT assign the binary path to a variable. Use the bare command or literal path.
122
+ - **Independent** operations (e.g. reading 3 emails): run as parallel Bash tool calls.
123
+ - **Sequential** operations (e.g. register then inbox): separate sequential Bash calls.
124
+
125
+ ## Error recovery
126
+
127
+ **Auth error** ("Missing X-Emcom-Name header"):
128
+ 1. `emcom names` — read the output, pick a name yourself.
129
+ 2. `emcom register --name <NAME>`
130
+ 3. Retry original command.
131
+
132
+ **Connection error** ("Connection refused", ECONNREFUSED):
133
+ 1. Start server: `emcom-server &` (or full path if not in PATH).
134
+ 2. `sleep 2`
135
+ 3. Retry.
136
+
137
+ **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.
138
+
139
+ **Stale local state** (you see "already registered" but server says otherwise): `emcom register --force --name <NAME>`. Use sparingly; usually the server is right.
140
+
141
+ ## Custom tag conventions
142
+
143
+ The team uses these custom tags by convention (your team may differ — check briefing.md or ask):
144
+
145
+ | Tag | Meaning |
146
+ |-----|---------|
147
+ | `urgent` | Needs response within minutes, not hours |
148
+ | `blocker` | Blocking another agent's progress; resolve first |
149
+ | `fyi` | No reply needed; informational only |
150
+ | `decided` | Decision recorded, future references can grep this |
151
+ | `parked` | Real but deferred; revisit when conditions change |
152
+
153
+ You may invent additional custom tags freely. Tags are cheap and grep-friendly.
154
+
155
+ ## Command reference
156
+
157
+ | User intent | Command |
158
+ |-------------|---------|
159
+ | "register", "join emcom" | `emcom register [--name NAME] [--description DESC] [--force]` |
160
+ | "unregister", "leave" | `emcom unregister` |
161
+ | "who's online", "who's here" | `emcom who` |
162
+ | "update my description" | `emcom update --description DESC` |
163
+ | "check email", "inbox" | `emcom inbox [--all]` |
164
+ | "read email X" | `emcom read ID [--tag TAG...]` |
165
+ | "send email to X" | `emcom send --to NAME [--cc NAME] --subject SUBJ --body BODY` |
166
+ | "reply to X" | `emcom reply ID --body BODY` |
167
+ | "show thread" | `emcom thread THREAD_ID` |
168
+ | "list threads" | `emcom threads` |
169
+ | "sent emails" | `emcom sent` |
170
+ | "all emails", "everything" | `emcom all` |
171
+ | "tag email X as Y" | `emcom tag ID TAG [TAG...]` |
172
+ | "remove tag Y from X" | `emcom untag ID TAG` |
173
+ | "find emails tagged Y" | `emcom tagged TAG` |
174
+ | "search for X" | `emcom search [--from NAME] [--to NAME] [--subject TEXT] [--tag TAG] [--body TEXT]` |
175
+ | "list available names" | `emcom names` |
176
+ | "add names to pool" | `emcom names --add NAME [NAME...]` |
177
+ | "purge", "clean out", "reset" | `emcom purge` |
178
+
179
+ ## Notes
180
+
181
+ - Short IDs (first 8 hex chars) work everywhere an ID is accepted.
182
+ - Server: port 8800 (override via `EMCOM_PORT`). Data in `~/.emcom/`.
183
+ - Always use `127.0.0.1` not `localhost` (avoids IPv6 DNS resolution penalty on Windows).
184
+ - `emcom all` shows unified sent+received view with `>>` (sent) / `<<` (received) markers.
185
+ - Backtick-containing bodies via `--body "..."` get shell-expanded — single-quote the body when it contains backticks or `$`.
186
+
187
+ ## Output
188
+
189
+ 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).