@vib795/agent-memory 0.6.3 → 0.6.4

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +20 -0
  3. package/src/setup.js +72 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vib795/agent-memory",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Durable cross-repo knowledge graph for GitHub Copilot and Claude Code. Markdown source of truth, disposable SQLite index, zero runtime dependencies.",
5
5
  "keywords": [
6
6
  "github-copilot",
package/src/cli.js CHANGED
@@ -164,6 +164,26 @@ function cmdSetup() {
164
164
  );
165
165
  }
166
166
 
167
+ // Naming what did not install is the whole point. A run that silently installs
168
+ // eleven of twelve reads as a success and sends the user back to an editor that is
169
+ // still missing a skill.
170
+ if (r.failed.length) {
171
+ lines.push('', ' Failed:');
172
+ for (const f of r.failed) lines.push(` [failed] ${f.path} — ${f.error}`);
173
+ lines.push(
174
+ '',
175
+ ' A path that will not clear is usually a leftover link from an install that has',
176
+ ' since moved. `agent-memory uninstall` reclaims those, then run setup again.',
177
+ );
178
+ }
179
+ if (r.compactError) {
180
+ lines.push(
181
+ '',
182
+ ` Skills are installed, but refreshing the /recall description failed: ${r.compactError}`,
183
+ ' Run `agent-memory index` then `agent-memory compact` to retry just that step.',
184
+ );
185
+ }
186
+
167
187
  return {
168
188
  ok: true,
169
189
  ...r,
package/src/setup.js CHANGED
@@ -55,11 +55,45 @@ function clear(path) {
55
55
  // end stay put — which is exactly what passing `recursive` here would risk.
56
56
  rmdirSync(path);
57
57
  }
58
+ // `force` tells rmSync to swallow ENOENT, and a junction whose target is gone can
59
+ // answer the stat rmSync makes with ENOENT while the reparse point itself stays on
60
+ // disk. The removal then reports success and changes nothing, after which
61
+ // symlinkSync fails EEXIST and the copy fallback lands on a path that still
62
+ // exists. Checking is what turns that silent no-op into an error naming the path.
63
+ if (isLink(path)) rmdirSync(path);
58
64
  } else if (existsSync(path)) {
59
65
  rmSync(path, { recursive: true, force: true });
60
66
  }
61
67
  }
62
68
 
69
+ /**
70
+ * Decide whether a skill link at a path we manage was put there by this tool.
71
+ *
72
+ * Identity cannot be "points at where we live right now". An install that has since
73
+ * moved, or a checkout that was deleted, leaves a link that test would disown — and a
74
+ * disowned link is unreclaimable: `uninstall` keeps it as "not ours" and `setup`
75
+ * cannot replace what it will not clear, so the user is left holding a broken link
76
+ * that no command in this tool will fix.
77
+ */
78
+ function ownsLink(link, name) {
79
+ let dest;
80
+ try {
81
+ dest = resolve(readlinkSync(link));
82
+ } catch {
83
+ // A link sitting at one of our paths whose target cannot even be read is ours to
84
+ // clear. Nothing downstream can use it either.
85
+ return true;
86
+ }
87
+ if (dest === join(packagedSkillsDir(), name)) return true;
88
+ // Dangling. Nothing is lost by reclaiming a link that points nowhere, and leaving it
89
+ // is precisely what deadlocks both commands.
90
+ if (!existsSync(dest)) return true;
91
+ // Live, but pointing into some other agent-memory tree — an older global install, or
92
+ // a checkout the user set up from. The signature is that its parent holds all three
93
+ // of our skills, which a hand-written skill directory would not.
94
+ return SKILLS.every((s) => existsSync(join(dirname(dest), s, 'SKILL.md')));
95
+ }
96
+
63
97
  /**
64
98
  * Link one skill into one agent directory.
65
99
  *
@@ -130,7 +164,6 @@ export function danglingSkillLinks() {
130
164
  * that indexed them; removing them is a separate, deliberate act.
131
165
  */
132
166
  export function unlinkSkills() {
133
- const packaged = packagedSkillsDir();
134
167
  const removed = [];
135
168
  const kept = [];
136
169
 
@@ -141,11 +174,9 @@ export function unlinkSkills() {
141
174
  if (!existsSync(link) && !isLink(link)) continue;
142
175
  let owned = false;
143
176
  try {
144
- owned = isLink(link)
145
- ? resolve(readlinkSync(link)) === join(packaged, name)
146
- : existsSync(join(link, 'SKILL.md'));
177
+ owned = isLink(link) ? ownsLink(link, name) : existsSync(join(link, 'SKILL.md'));
147
178
  } catch {
148
- // A link we cannot read is a link we cannot claim. Leave it.
179
+ // A directory we cannot stat is one we cannot claim. Leave it.
149
180
  owned = false;
150
181
  }
151
182
  if (owned) {
@@ -182,15 +213,30 @@ export function setup({ compactFn } = {}) {
182
213
  const targets = installableTargets();
183
214
  const installed = [];
184
215
  const copies = [];
216
+ const failed = [];
185
217
 
186
218
  for (const target of targets) {
187
219
  for (const name of SKILLS) {
188
- const r =
189
- target.kind === 'skill-dir'
190
- ? linkSkill(name, target.dir)
191
- : writePromptFile(name, target.dir);
192
- installed.push({ ...r, target: target.id, label: target.label });
193
- if (r.mode === 'copy') copies.push(r);
220
+ // Isolated per skill. One unwritable target used to abort the whole run and
221
+ // discard the report with it, so a user whose `.copilot` link was wedged got no
222
+ // output at all and no hint that the other eleven had been fine. A failure here
223
+ // is data to print, not a reason to stop.
224
+ try {
225
+ const r =
226
+ target.kind === 'skill-dir'
227
+ ? linkSkill(name, target.dir)
228
+ : writePromptFile(name, target.dir);
229
+ installed.push({ ...r, target: target.id, label: target.label });
230
+ if (r.mode === 'copy') copies.push(r);
231
+ } catch (err) {
232
+ failed.push({
233
+ name,
234
+ target: target.id,
235
+ label: target.label,
236
+ path: join(target.dir, target.kind === 'skill-dir' ? name : `${name}.prompt.md`),
237
+ error: err.message,
238
+ });
239
+ }
194
240
  }
195
241
  }
196
242
 
@@ -209,11 +255,25 @@ export function setup({ compactFn } = {}) {
209
255
 
210
256
  // compact is passed in so this module does not pull the database into memory just
211
257
  // to make some symlinks.
212
- const result = compactFn ? compactFn() : null;
258
+ //
259
+ // Its failure must not sink the run. Linking is already done and written to disk by
260
+ // this point, so throwing here would throw away an accurate report of work that
261
+ // actually happened and leave the user with no idea any of it succeeded. compact
262
+ // touches every registered skill path, which is exactly the set most likely to hold
263
+ // a stale entry, so it is the step most likely to throw.
264
+ let result = null;
265
+ let compactError = null;
266
+ try {
267
+ result = compactFn ? compactFn() : null;
268
+ } catch (err) {
269
+ compactError = err.message;
270
+ }
213
271
  return {
214
272
  targets,
215
273
  installed,
216
274
  copies,
275
+ failed,
276
+ compactError,
217
277
  skillPaths,
218
278
  home: agentHome(),
219
279
  digest: result?.digest ?? null,