fellow-agents 0.0.16 → 0.0.19
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 +13 -7
- package/dist/commands/uninstall.js +10 -4
- package/dist/lib/services.js +54 -0
- package/dist/lib/skills.js +66 -22
- package/package.json +1 -1
- package/skills/emcom/SKILL.md +3 -5
package/dist/commands/start.js
CHANGED
|
@@ -97,14 +97,20 @@ export async function start(opts) {
|
|
|
97
97
|
// 5. Install AI skills (SKILL.md files) to known CLI paths
|
|
98
98
|
console.log("[5/8] Installing skills...");
|
|
99
99
|
const skillResult = installSkills();
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const skillTotal = skillResult.written.length + skillResult.refreshed.length + skillResult.skipped.length;
|
|
101
|
+
if (skillTotal === 0) {
|
|
102
|
+
console.log(" No bundled skills");
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
else {
|
|
105
|
+
if (skillResult.written.length > 0) {
|
|
106
|
+
console.log(` Installed ${skillResult.written.length} skill file(s)`);
|
|
107
|
+
}
|
|
108
|
+
if (skillResult.refreshed.length > 0) {
|
|
109
|
+
console.log(` Refreshed ${skillResult.refreshed.length} skill file(s) to latest`);
|
|
110
|
+
}
|
|
111
|
+
if (skillResult.skipped.length > 0) {
|
|
112
|
+
console.log(` Preserved ${skillResult.skipped.length} existing skill file(s) — customized or unowned`);
|
|
113
|
+
}
|
|
108
114
|
}
|
|
109
115
|
// PATH trick: prepend bin dir so agents find emcom/tracker
|
|
110
116
|
const env = { ...process.env, PATH: `${binDir}${process.platform === "win32" ? ";" : ":"}${process.env.PATH}` };
|
|
@@ -86,11 +86,17 @@ export function uninstall(opts) {
|
|
|
86
86
|
// Remove skills we installed (only the ones that match the shipped bytes —
|
|
87
87
|
// user-customized files are preserved).
|
|
88
88
|
const skillResult = uninstallSkills();
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
const skillTotal = skillResult.removed.length + skillResult.preserved.length;
|
|
90
|
+
if (skillTotal === 0) {
|
|
91
|
+
console.log(" No fellow-agents-tracked skill files found");
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
else {
|
|
94
|
+
if (skillResult.removed.length > 0) {
|
|
95
|
+
console.log(` Removed ${skillResult.removed.length} skill file(s)`);
|
|
96
|
+
}
|
|
97
|
+
if (skillResult.preserved.length > 0) {
|
|
98
|
+
console.log(` Preserved ${skillResult.preserved.length} customized/unowned skill file(s)`);
|
|
99
|
+
}
|
|
94
100
|
}
|
|
95
101
|
for (const t of targets) {
|
|
96
102
|
try {
|
package/dist/lib/services.js
CHANGED
|
@@ -89,6 +89,52 @@ function killTree(pid) {
|
|
|
89
89
|
process.kill(pid);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
// Scan a port for any listening PID and kill it. Used as a fallback when our
|
|
93
|
+
// PID files are missing (e.g., user ran clean) but a previous service is still
|
|
94
|
+
// holding the port — produces orphans that block re-start and re-uninstall.
|
|
95
|
+
function killOnPort(port) {
|
|
96
|
+
const killed = [];
|
|
97
|
+
try {
|
|
98
|
+
if (process.platform === "win32") {
|
|
99
|
+
const output = execSync(`netstat -ano -p tcp`, { stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
100
|
+
const pids = new Set();
|
|
101
|
+
for (const line of output.split("\n")) {
|
|
102
|
+
const trimmed = line.trim();
|
|
103
|
+
// Match "127.0.0.1:8800 ... LISTENING <pid>"
|
|
104
|
+
if (trimmed.includes(`:${port}`) && trimmed.includes("LISTENING")) {
|
|
105
|
+
const parts = trimmed.split(/\s+/);
|
|
106
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
107
|
+
if (!isNaN(pid) && pid > 4)
|
|
108
|
+
pids.add(pid); // skip system PIDs 0/4
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const pid of pids) {
|
|
112
|
+
try {
|
|
113
|
+
execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore" });
|
|
114
|
+
killed.push(pid);
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const output = execSync(`lsof -ti:${port}`, { stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
121
|
+
for (const line of output.split("\n")) {
|
|
122
|
+
const pid = parseInt(line.trim(), 10);
|
|
123
|
+
if (!isNaN(pid)) {
|
|
124
|
+
try {
|
|
125
|
+
process.kill(pid, "SIGKILL");
|
|
126
|
+
killed.push(pid);
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// No output (no process listening) or command failed — return empty
|
|
135
|
+
}
|
|
136
|
+
return killed;
|
|
137
|
+
}
|
|
92
138
|
export function stopAll() {
|
|
93
139
|
for (const name of ["emcom-server", "pty-win"]) {
|
|
94
140
|
const pid = readPid(name);
|
|
@@ -106,6 +152,14 @@ export function stopAll() {
|
|
|
106
152
|
}
|
|
107
153
|
removePid(name);
|
|
108
154
|
}
|
|
155
|
+
// Fallback: scan default ports for orphans whose PID files have been lost.
|
|
156
|
+
// Common after `fellow-agents clean` or manual `Remove-Item` of pid/ dir.
|
|
157
|
+
for (const port of [3700, 8800]) {
|
|
158
|
+
const orphans = killOnPort(port);
|
|
159
|
+
if (orphans.length > 0) {
|
|
160
|
+
console.log(` Killed orphan(s) on :${port} (pid ${orphans.join(", ")})`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
109
163
|
}
|
|
110
164
|
export function waitForHealth(url, timeoutMs = 30000) {
|
|
111
165
|
const mod = url.startsWith("https") ? https : http;
|
package/dist/lib/skills.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, readdirSync, mkdirSync, copyFileSync, readFileSync, statSync, rmSync, rmdirSync } from "fs";
|
|
1
|
+
import { existsSync, readdirSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, statSync, rmSync, rmdirSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
+
import { createHash } from "crypto";
|
|
4
5
|
import { skillsDir } from "./paths.js";
|
|
5
6
|
// Three target paths per agentskills.io convention — installed CLIs vary; we write to all three
|
|
6
7
|
// so the skill works regardless of which AI CLI the user is using.
|
|
@@ -9,15 +10,24 @@ const targetRoots = [
|
|
|
9
10
|
join(homedir(), ".copilot", "skills"), // GitHub Copilot CLI
|
|
10
11
|
join(homedir(), ".agents", "skills"), // pi + cross-tool universal location
|
|
11
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";
|
|
12
17
|
/**
|
|
13
18
|
* Copy bundled skills to all known target paths.
|
|
14
19
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|
|
18
28
|
*/
|
|
19
29
|
export function installSkills() {
|
|
20
|
-
const result = { written: [], skipped: [] };
|
|
30
|
+
const result = { written: [], refreshed: [], skipped: [] };
|
|
21
31
|
if (!existsSync(skillsDir))
|
|
22
32
|
return result;
|
|
23
33
|
const skillNames = readdirSync(skillsDir).filter((name) => {
|
|
@@ -33,16 +43,42 @@ export function installSkills() {
|
|
|
33
43
|
const skillFiles = walkSkillFiles(sourceSkillDir);
|
|
34
44
|
for (const relPath of skillFiles) {
|
|
35
45
|
const sourceFile = join(sourceSkillDir, relPath);
|
|
46
|
+
const sourceSha = sha256File(sourceFile);
|
|
36
47
|
for (const root of targetRoots) {
|
|
37
48
|
const targetFile = join(root, skillName, relPath);
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
40
61
|
result.skipped.push(targetFile);
|
|
41
62
|
continue;
|
|
42
63
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
}
|
|
46
82
|
}
|
|
47
83
|
}
|
|
48
84
|
}
|
|
@@ -51,13 +87,15 @@ export function installSkills() {
|
|
|
51
87
|
/**
|
|
52
88
|
* Remove skill files we installed, if the user hasn't modified them.
|
|
53
89
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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.
|
|
57
96
|
*
|
|
58
97
|
* After file removal, attempts to clean up empty skill directories and
|
|
59
|
-
* empty target root directories
|
|
60
|
-
* have Copilot installed).
|
|
98
|
+
* empty target root directories.
|
|
61
99
|
*/
|
|
62
100
|
export function uninstallSkills() {
|
|
63
101
|
const result = { removed: [], preserved: [] };
|
|
@@ -79,15 +117,21 @@ export function uninstallSkills() {
|
|
|
79
117
|
if (!existsSync(targetSkillDir))
|
|
80
118
|
continue;
|
|
81
119
|
for (const relPath of skillFiles) {
|
|
82
|
-
const sourceFile = join(sourceSkillDir, relPath);
|
|
83
120
|
const targetFile = join(targetSkillDir, relPath);
|
|
121
|
+
const sidecarFile = targetFile + SIDECAR_SUFFIX;
|
|
84
122
|
if (!existsSync(targetFile))
|
|
85
123
|
continue;
|
|
124
|
+
if (!existsSync(sidecarFile)) {
|
|
125
|
+
// No sidecar — can't prove ownership, preserve
|
|
126
|
+
result.preserved.push(targetFile);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
86
129
|
try {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
130
|
+
const recordedSha = readFileSync(sidecarFile, "utf-8").trim();
|
|
131
|
+
const currentSha = sha256File(targetFile);
|
|
132
|
+
if (currentSha === recordedSha) {
|
|
90
133
|
rmSync(targetFile);
|
|
134
|
+
rmSync(sidecarFile);
|
|
91
135
|
result.removed.push(targetFile);
|
|
92
136
|
}
|
|
93
137
|
else {
|
|
@@ -95,18 +139,18 @@ export function uninstallSkills() {
|
|
|
95
139
|
}
|
|
96
140
|
}
|
|
97
141
|
catch {
|
|
98
|
-
// Can't read either file — skip, don't risk data loss
|
|
99
142
|
result.preserved.push(targetFile);
|
|
100
143
|
}
|
|
101
144
|
}
|
|
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
145
|
tryRemoveIfEmpty(targetSkillDir);
|
|
105
146
|
tryRemoveIfEmpty(root);
|
|
106
147
|
}
|
|
107
148
|
}
|
|
108
149
|
return result;
|
|
109
150
|
}
|
|
151
|
+
function sha256File(path) {
|
|
152
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
153
|
+
}
|
|
110
154
|
function tryRemoveIfEmpty(dir) {
|
|
111
155
|
try {
|
|
112
156
|
if (existsSync(dir) && readdirSync(dir).length === 0) {
|
package/package.json
CHANGED
package/skills/emcom/SKILL.md
CHANGED
|
@@ -111,11 +111,9 @@ Don't reply blind to in-flight work. A short delay to read the thread first prev
|
|
|
111
111
|
emcom <subcommand> [args]
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
- `~/.agents/skills/emcom/bin/emcom` (pi / universal skill install)
|
|
118
|
-
- Or whatever PATH was set up by `npm install -g fellow-agents`
|
|
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/`.
|
|
119
117
|
|
|
120
118
|
**Permission-friendly invocation** (matters in some CLIs that gate command execution):
|
|
121
119
|
|