@vib795/agent-memory 0.3.0 → 0.3.1

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/README.md CHANGED
@@ -162,6 +162,17 @@ exactly what would go.
162
162
  Everything already captured stays in `default`, at the same path as before. Nothing
163
163
  migrates and nothing changes until you create a second engagement.
164
164
 
165
+ **Two things sit outside the boundary, deliberately.** Skill installation is shared,
166
+ because every engagement reads the same three skill files and registering them per
167
+ engagement would leave the description stale after a switch. Handoff files are shared
168
+ too — they live in `~/.agents/handoffs`, not in a store — so `purge` does not remove
169
+ them and says so every time rather than letting "the store is gone" be mistaken for
170
+ "their context is gone".
171
+
172
+ Most people need none of this. If each client already gives you a separate machine,
173
+ that boundary is stronger than anything here, and the default engagement is all you
174
+ will ever touch.
175
+
165
176
  ## CLI
166
177
 
167
178
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vib795/agent-memory",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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
@@ -15,7 +15,7 @@ import { compact, maybeCompact } from './compact.js';
15
15
  import { staleness, currentRepo, reviewCandidates, captureGap } from './staleness.js';
16
16
  import { setup as runSetup, unlinkSkills, danglingSkillLinks, SKILLS } from './setup.js';
17
17
  import { detectTargets, installableTargets } from './targets.js';
18
- import { join } from 'node:path';
18
+ import { join, dirname } from 'node:path';
19
19
  import { atomicWrite } from './atomic.js';
20
20
 
21
21
  /**
@@ -807,6 +807,11 @@ function cmdEngagement(opts) {
807
807
  text: [
808
808
  `Purged engagement ${name}: ${notes} note${notes === 1 ? '' : 's'} removed.`,
809
809
  `${dir} no longer exists.`,
810
+ '',
811
+ // Said every time, because the gap between "the store is gone" and "their
812
+ // knowledge is gone" is exactly the assumption someone would make here.
813
+ 'Handoff files are not part of an engagement store and were not touched.',
814
+ `Review ${join(dirname(STORE_BASE), 'handoffs')} yourself.`,
810
815
  ].join('\n'),
811
816
  };
812
817
  }
package/src/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync } from 'node:fs';
1
+ import { readFileSync, existsSync, mkdirSync } from 'node:fs';
2
2
  import { join, dirname, parse as parsePath } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { atomicWrite } from './atomic.js';
@@ -85,6 +85,8 @@ export const paths = {
85
85
  db: join(STORE_ROOT, 'index.db'),
86
86
  routing: join(STORE_ROOT, 'ROUTING.md'),
87
87
  config: join(STORE_ROOT, 'config.json'),
88
+ // Machine-scoped keys live beside every engagement rather than inside one.
89
+ machineConfig: join(STORE_BASE, 'config.json'),
88
90
  typeDir: (type) => join(STORE_ROOT, 'notes', type),
89
91
  archiveTypeDir: (type) => join(STORE_ROOT, 'notes', 'archive', type),
90
92
  };
@@ -107,24 +109,41 @@ export const DEFAULTS = {
107
109
  // from a checkout or copied into place, and both are legitimate installs.
108
110
  export const LIST_KEYS = ['skillPaths'];
109
111
 
110
- export function loadConfig() {
111
- if (!existsSync(paths.config)) return { ...DEFAULTS, skillPaths: [] };
112
+ /** A malformed config must not take the store down. Absent and unreadable are equal. */
113
+ function readJson(file) {
114
+ if (!existsSync(file)) return {};
112
115
  try {
113
- const raw = JSON.parse(readFileSync(paths.config, 'utf8'));
114
- const merged = { ...DEFAULTS, skillPaths: [] };
115
- for (const [k, v] of Object.entries(raw)) {
116
- if (k in DEFAULTS && typeof v === 'number' && Number.isFinite(v) && v > 0) merged[k] = v;
117
- if (LIST_KEYS.includes(k) && Array.isArray(v)) {
118
- merged[k] = v.filter((s) => typeof s === 'string' && s.trim());
119
- }
120
- }
121
- return merged;
116
+ return JSON.parse(readFileSync(file, 'utf8'));
122
117
  } catch {
123
- // A malformed config must not take the store down. Defaults are always valid.
124
- return { ...DEFAULTS, skillPaths: [] };
118
+ return {};
125
119
  }
126
120
  }
127
121
 
122
+ /**
123
+ * Config comes from two files, split by what each key actually describes.
124
+ *
125
+ * Where the skills are installed is a fact about this machine. Every engagement
126
+ * shares one set of skill files, so registering them per engagement meant `compact`
127
+ * had nothing to regenerate after a switch and the recall description kept
128
+ * advertising the previous engagement's topics — into the agent's context, which is
129
+ * the worst place for it. Caps like the digest size really are per-store, and stay
130
+ * there. For the default engagement both files are the same path and nothing moves.
131
+ */
132
+ export function loadConfig() {
133
+ const merged = { ...DEFAULTS, skillPaths: [] };
134
+
135
+ for (const [k, v] of Object.entries(readJson(paths.config))) {
136
+ if (k in DEFAULTS && typeof v === 'number' && Number.isFinite(v) && v > 0) merged[k] = v;
137
+ }
138
+ const machine = readJson(paths.machineConfig);
139
+ for (const k of LIST_KEYS) {
140
+ if (Array.isArray(machine[k])) {
141
+ merged[k] = machine[k].filter((s) => typeof s === 'string' && s.trim());
142
+ }
143
+ }
144
+ return merged;
145
+ }
146
+
128
147
  /**
129
148
  * Merge a patch into config.json.
130
149
  *
@@ -136,24 +155,37 @@ export function loadConfig() {
136
155
  * cannot quietly become permanent state.
137
156
  */
138
157
  export function saveConfig(patch) {
139
- const current = existsSync(paths.config)
140
- ? (() => {
141
- try {
142
- return JSON.parse(readFileSync(paths.config, 'utf8'));
143
- } catch {
144
- return {};
145
- }
146
- })()
147
- : {};
148
-
149
- const next = { ...current };
158
+ const sameFile = paths.config === paths.machineConfig;
159
+ const caps = readJson(paths.config);
160
+ const machine = sameFile ? caps : readJson(paths.machineConfig);
161
+
162
+ let capsDirty = false;
163
+ let machineDirty = false;
150
164
  for (const [k, v] of Object.entries(patch || {})) {
151
- if (k in DEFAULTS && typeof v === 'number' && Number.isFinite(v) && v > 0) next[k] = v;
165
+ if (k in DEFAULTS && typeof v === 'number' && Number.isFinite(v) && v > 0) {
166
+ caps[k] = v;
167
+ capsDirty = true;
168
+ }
152
169
  if (LIST_KEYS.includes(k) && Array.isArray(v)) {
153
- next[k] = [...new Set(v.filter((s) => typeof s === 'string' && s.trim()))].sort();
170
+ machine[k] = [...new Set(v.filter((s) => typeof s === 'string' && s.trim()))].sort();
171
+ machineDirty = true;
154
172
  }
155
173
  }
156
174
 
157
- atomicWrite(paths.config, `${JSON.stringify(next, null, 2)}\n`);
158
- return next;
175
+ if (sameFile) {
176
+ if (capsDirty || machineDirty) {
177
+ mkdirSync(dirname(paths.config), { recursive: true });
178
+ atomicWrite(paths.config, `${JSON.stringify(caps, null, 2)}\n`);
179
+ }
180
+ return { ...caps };
181
+ }
182
+ if (capsDirty) {
183
+ mkdirSync(dirname(paths.config), { recursive: true });
184
+ atomicWrite(paths.config, `${JSON.stringify(caps, null, 2)}\n`);
185
+ }
186
+ if (machineDirty) {
187
+ mkdirSync(dirname(paths.machineConfig), { recursive: true });
188
+ atomicWrite(paths.machineConfig, `${JSON.stringify(machine, null, 2)}\n`);
189
+ }
190
+ return { ...caps, ...Object.fromEntries(LIST_KEYS.map((k) => [k, machine[k]]).filter(([, v]) => v)) };
159
191
  }