@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 +11 -0
- package/package.json +1 -1
- package/src/cli.js +6 -1
- package/src/config.js +61 -29
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.
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)
|
|
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
|
-
|
|
170
|
+
machine[k] = [...new Set(v.filter((s) => typeof s === 'string' && s.trim()))].sort();
|
|
171
|
+
machineDirty = true;
|
|
154
172
|
}
|
|
155
173
|
}
|
|
156
174
|
|
|
157
|
-
|
|
158
|
-
|
|
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
|
}
|