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.
@@ -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) — delete to refresh`);
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
@@ -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
- * 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.
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
- if (existsSync(targetFile)) {
39
- // User-customized or already-installed — never overwrite
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
- mkdirSync(join(root, skillName, ...relPath.split(/[\\/]/).slice(0, -1)), { recursive: true });
44
- copyFileSync(sourceFile, targetFile);
45
- result.written.push(targetFile);
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
- * 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).
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 (e.g., ~/.copilot/skills/ if user doesn't
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 sourceBytes = readFileSync(sourceFile);
88
- const targetBytes = readFileSync(targetFile);
89
- if (Buffer.compare(sourceBytes, targetBytes) === 0) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fellow-agents",
3
- "version": "0.0.16",
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": {
@@ -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
- 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`
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