fellow-agents 0.0.16 → 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.
- package/dist/commands/start.js +5 -2
- 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
|
@@ -100,10 +100,13 @@ export async function start(opts) {
|
|
|
100
100
|
if (skillResult.written.length > 0) {
|
|
101
101
|
console.log(` Installed ${skillResult.written.length} skill file(s)`);
|
|
102
102
|
}
|
|
103
|
+
if (skillResult.refreshed.length > 0) {
|
|
104
|
+
console.log(` Refreshed ${skillResult.refreshed.length} skill file(s) to latest`);
|
|
105
|
+
}
|
|
103
106
|
if (skillResult.skipped.length > 0) {
|
|
104
|
-
console.log(` Preserved ${skillResult.skipped.length} existing skill file(s) —
|
|
107
|
+
console.log(` Preserved ${skillResult.skipped.length} existing skill file(s) — customized or unowned`);
|
|
105
108
|
}
|
|
106
|
-
if (skillResult.written.length === 0 && skillResult.skipped.length === 0) {
|
|
109
|
+
if (skillResult.written.length === 0 && skillResult.refreshed.length === 0 && skillResult.skipped.length === 0) {
|
|
107
110
|
console.log(" No skills bundled");
|
|
108
111
|
}
|
|
109
112
|
// PATH trick: prepend bin dir so agents find emcom/tracker
|
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
|
|