@vuau/agent-memory 0.1.0 → 0.1.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/dist/bin/cli.js +243 -0
- package/dist/index.js +348 -0
- package/package.json +14 -8
- package/bin/cli.ts +0 -111
- package/index.ts +0 -11
- package/src/core/doctor.ts +0 -86
- package/src/core/index.ts +0 -5
- package/src/core/memory.ts +0 -86
- package/src/core/scaffold.ts +0 -124
- package/src/core/tasks.ts +0 -77
- package/src/core/types.ts +0 -30
- package/src/opencode/plugin.ts +0 -97
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core/scaffold.ts
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
5
|
+
import { join, resolve, dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
// src/core/types.ts
|
|
9
|
+
var AGENTS_DIR = ".agents";
|
|
10
|
+
var SPEC_DIR = ".agents/spec";
|
|
11
|
+
var MEMORY_FILE = ".agents/MEMORY.md";
|
|
12
|
+
var TASKS_FILE = ".agents/TASKS.md";
|
|
13
|
+
var AGENTS_MD = "AGENTS.md";
|
|
14
|
+
var COPILOT_INSTRUCTIONS = ".github/copilot-instructions.md";
|
|
15
|
+
|
|
16
|
+
// src/core/scaffold.ts
|
|
17
|
+
function getTemplatesDir() {
|
|
18
|
+
try {
|
|
19
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const candidate = resolve(thisDir, "../../templates");
|
|
21
|
+
if (existsSync(candidate)) return candidate;
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
const candidate2 = resolve(__dirname, "../../templates");
|
|
25
|
+
if (existsSync(candidate2)) return candidate2;
|
|
26
|
+
throw new Error("Cannot locate templates directory");
|
|
27
|
+
}
|
|
28
|
+
var TEMPLATES_DIR = getTemplatesDir();
|
|
29
|
+
function readTemplate(name) {
|
|
30
|
+
const templatePath = join(TEMPLATES_DIR, name);
|
|
31
|
+
return readFileSync(templatePath, "utf-8");
|
|
32
|
+
}
|
|
33
|
+
function applyVars(content, vars) {
|
|
34
|
+
let result = content;
|
|
35
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
36
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function scaffold(projectDir, config = {}, force = false) {
|
|
41
|
+
const result = { created: [], skipped: [] };
|
|
42
|
+
const projectName = config.projectName || guessProjectName(projectDir);
|
|
43
|
+
const vars = { PROJECT_NAME: projectName };
|
|
44
|
+
const dirs = [
|
|
45
|
+
join(projectDir, AGENTS_DIR),
|
|
46
|
+
join(projectDir, SPEC_DIR)
|
|
47
|
+
];
|
|
48
|
+
if (config.copilotInstructions !== false) {
|
|
49
|
+
dirs.push(join(projectDir, ".github"));
|
|
50
|
+
}
|
|
51
|
+
for (const dir of dirs) {
|
|
52
|
+
if (!existsSync(dir)) {
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const files = [
|
|
57
|
+
{ target: AGENTS_MD, template: "AGENTS.md" },
|
|
58
|
+
{ target: MEMORY_FILE, template: "MEMORY.md" },
|
|
59
|
+
{ target: TASKS_FILE, template: "TASKS.md" }
|
|
60
|
+
];
|
|
61
|
+
if (config.copilotInstructions !== false) {
|
|
62
|
+
files.push({
|
|
63
|
+
target: COPILOT_INSTRUCTIONS,
|
|
64
|
+
template: "copilot-instructions.md"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const { target, template } of files) {
|
|
68
|
+
const targetPath = join(projectDir, target);
|
|
69
|
+
if (existsSync(targetPath) && !force) {
|
|
70
|
+
result.skipped.push(target);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const content = applyVars(readTemplate(template), vars);
|
|
74
|
+
writeFileSync(targetPath, content);
|
|
75
|
+
result.created.push(target);
|
|
76
|
+
}
|
|
77
|
+
const specKeep = join(projectDir, SPEC_DIR, ".gitkeep");
|
|
78
|
+
if (!existsSync(specKeep)) {
|
|
79
|
+
writeFileSync(specKeep, "");
|
|
80
|
+
result.created.push(`${SPEC_DIR}/.gitkeep`);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
function guessProjectName(dir) {
|
|
85
|
+
const pkgPath = join(dir, "package.json");
|
|
86
|
+
if (existsSync(pkgPath)) {
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
89
|
+
if (pkg.name) return pkg.name;
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return dir.split("/").pop() || "Project";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/core/doctor.ts
|
|
97
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
98
|
+
import { join as join2 } from "path";
|
|
99
|
+
function doctor(projectDir) {
|
|
100
|
+
const issues = [];
|
|
101
|
+
const required = [
|
|
102
|
+
{ file: AGENTS_MD, desc: "Root router file" },
|
|
103
|
+
{ file: MEMORY_FILE, desc: "Long-term memory" },
|
|
104
|
+
{ file: TASKS_FILE, desc: "Working memory" }
|
|
105
|
+
];
|
|
106
|
+
for (const { file, desc } of required) {
|
|
107
|
+
const filePath = join2(projectDir, file);
|
|
108
|
+
if (!existsSync2(filePath)) {
|
|
109
|
+
issues.push({ level: "error", file, message: `Missing ${desc}` });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const dir of [AGENTS_DIR, SPEC_DIR]) {
|
|
113
|
+
if (!existsSync2(join2(projectDir, dir))) {
|
|
114
|
+
issues.push({ level: "error", file: dir, message: "Directory missing" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const copilotPath = join2(projectDir, COPILOT_INSTRUCTIONS);
|
|
118
|
+
if (!existsSync2(copilotPath)) {
|
|
119
|
+
issues.push({
|
|
120
|
+
level: "warning",
|
|
121
|
+
file: COPILOT_INSTRUCTIONS,
|
|
122
|
+
message: "Copilot instructions missing \u2014 VSCode/GitHub Copilot won't have context"
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const agentsPath = join2(projectDir, AGENTS_MD);
|
|
126
|
+
if (existsSync2(agentsPath)) {
|
|
127
|
+
const content = readFileSync2(agentsPath, "utf-8");
|
|
128
|
+
if (!content.includes(".agents/")) {
|
|
129
|
+
issues.push({
|
|
130
|
+
level: "warning",
|
|
131
|
+
file: AGENTS_MD,
|
|
132
|
+
message: "No references to .agents/ \u2014 agents may not find memory files"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (content.split("\n").length > 150) {
|
|
136
|
+
issues.push({
|
|
137
|
+
level: "warning",
|
|
138
|
+
file: AGENTS_MD,
|
|
139
|
+
message: "Over 150 lines \u2014 consider keeping it concise as a router"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const memoryPath = join2(projectDir, MEMORY_FILE);
|
|
144
|
+
if (existsSync2(memoryPath)) {
|
|
145
|
+
const lines = readFileSync2(memoryPath, "utf-8").split("\n").length;
|
|
146
|
+
if (lines > 150) {
|
|
147
|
+
issues.push({
|
|
148
|
+
level: "warning",
|
|
149
|
+
file: MEMORY_FILE,
|
|
150
|
+
message: `${lines} lines \u2014 consider compressing or archiving old entries`
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { ok: issues.filter((i) => i.level === "error").length === 0, issues };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// bin/cli.ts
|
|
158
|
+
var args = process.argv.slice(2);
|
|
159
|
+
var command = args[0];
|
|
160
|
+
function printUsage() {
|
|
161
|
+
console.log(`
|
|
162
|
+
@vuau/agent-memory \u2014 Structured AI memory for codebases
|
|
163
|
+
|
|
164
|
+
Usage:
|
|
165
|
+
agent-memory init [options] Scaffold .agents/ structure
|
|
166
|
+
agent-memory doctor Validate .agents/ structure
|
|
167
|
+
agent-memory help Show this help
|
|
168
|
+
|
|
169
|
+
Options (init):
|
|
170
|
+
--force Overwrite existing files
|
|
171
|
+
--name <name> Project name (default: from package.json)
|
|
172
|
+
--no-copilot Skip .github/copilot-instructions.md
|
|
173
|
+
`);
|
|
174
|
+
}
|
|
175
|
+
function runInit() {
|
|
176
|
+
const force = args.includes("--force");
|
|
177
|
+
const noCopilot = args.includes("--no-copilot");
|
|
178
|
+
const nameIdx = args.indexOf("--name");
|
|
179
|
+
const projectName = nameIdx !== -1 ? args[nameIdx + 1] : void 0;
|
|
180
|
+
const cwd = process.cwd();
|
|
181
|
+
console.log(`Initializing agent memory in ${cwd}...`);
|
|
182
|
+
const result = scaffold(cwd, {
|
|
183
|
+
projectName,
|
|
184
|
+
copilotInstructions: !noCopilot
|
|
185
|
+
}, force);
|
|
186
|
+
if (result.created.length > 0) {
|
|
187
|
+
console.log("\nCreated:");
|
|
188
|
+
for (const f of result.created) {
|
|
189
|
+
console.log(` \u2713 ${f}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (result.skipped.length > 0) {
|
|
193
|
+
console.log("\nSkipped (already exist):");
|
|
194
|
+
for (const f of result.skipped) {
|
|
195
|
+
console.log(` - ${f}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (result.created.length === 0 && result.skipped.length > 0) {
|
|
199
|
+
console.log("\nAll files already exist. Use --force to overwrite.");
|
|
200
|
+
}
|
|
201
|
+
console.log("\nNext steps:");
|
|
202
|
+
console.log(" 1. Edit AGENTS.md \u2014 add your project-specific rules");
|
|
203
|
+
console.log(" 2. Add spec files to .agents/spec/ for detailed documentation");
|
|
204
|
+
console.log(' 3. For OpenCode: add to opencode.json \u2192 { "plugin": ["@vuau/agent-memory"] }');
|
|
205
|
+
console.log("");
|
|
206
|
+
}
|
|
207
|
+
function runDoctor() {
|
|
208
|
+
const cwd = process.cwd();
|
|
209
|
+
const result = doctor(cwd);
|
|
210
|
+
if (result.issues.length === 0) {
|
|
211
|
+
console.log("\u2713 All checks passed!");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
for (const issue of result.issues) {
|
|
215
|
+
const icon = issue.level === "error" ? "\u2717" : issue.level === "warning" ? "\u26A0" : "\u2139";
|
|
216
|
+
console.log(` ${icon} [${issue.level}] ${issue.file}: ${issue.message}`);
|
|
217
|
+
}
|
|
218
|
+
console.log("");
|
|
219
|
+
if (result.ok) {
|
|
220
|
+
console.log("\u26A0 Passed with warnings. Run 'agent-memory init' to fix missing files.");
|
|
221
|
+
} else {
|
|
222
|
+
console.log("\u2717 Failed. Run 'agent-memory init' to create missing files.");
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
switch (command) {
|
|
227
|
+
case "init":
|
|
228
|
+
runInit();
|
|
229
|
+
break;
|
|
230
|
+
case "doctor":
|
|
231
|
+
runDoctor();
|
|
232
|
+
break;
|
|
233
|
+
case "help":
|
|
234
|
+
case "--help":
|
|
235
|
+
case "-h":
|
|
236
|
+
case void 0:
|
|
237
|
+
printUsage();
|
|
238
|
+
break;
|
|
239
|
+
default:
|
|
240
|
+
console.error(`Unknown command: ${command}`);
|
|
241
|
+
printUsage();
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// src/opencode/plugin.ts
|
|
2
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
3
|
+
import { resolve as resolve2 } from "path";
|
|
4
|
+
|
|
5
|
+
// src/core/scaffold.ts
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
7
|
+
import { join, resolve, dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
// src/core/types.ts
|
|
11
|
+
var AGENTS_DIR = ".agents";
|
|
12
|
+
var SPEC_DIR = ".agents/spec";
|
|
13
|
+
var MEMORY_FILE = ".agents/MEMORY.md";
|
|
14
|
+
var TASKS_FILE = ".agents/TASKS.md";
|
|
15
|
+
var AGENTS_MD = "AGENTS.md";
|
|
16
|
+
var COPILOT_INSTRUCTIONS = ".github/copilot-instructions.md";
|
|
17
|
+
|
|
18
|
+
// src/core/scaffold.ts
|
|
19
|
+
function getTemplatesDir() {
|
|
20
|
+
try {
|
|
21
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const candidate = resolve(thisDir, "../../templates");
|
|
23
|
+
if (existsSync(candidate)) return candidate;
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
const candidate2 = resolve(__dirname, "../../templates");
|
|
27
|
+
if (existsSync(candidate2)) return candidate2;
|
|
28
|
+
throw new Error("Cannot locate templates directory");
|
|
29
|
+
}
|
|
30
|
+
var TEMPLATES_DIR = getTemplatesDir();
|
|
31
|
+
function readTemplate(name) {
|
|
32
|
+
const templatePath = join(TEMPLATES_DIR, name);
|
|
33
|
+
return readFileSync(templatePath, "utf-8");
|
|
34
|
+
}
|
|
35
|
+
function applyVars(content, vars) {
|
|
36
|
+
let result = content;
|
|
37
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
38
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function scaffold(projectDir, config = {}, force = false) {
|
|
43
|
+
const result = { created: [], skipped: [] };
|
|
44
|
+
const projectName = config.projectName || guessProjectName(projectDir);
|
|
45
|
+
const vars = { PROJECT_NAME: projectName };
|
|
46
|
+
const dirs = [
|
|
47
|
+
join(projectDir, AGENTS_DIR),
|
|
48
|
+
join(projectDir, SPEC_DIR)
|
|
49
|
+
];
|
|
50
|
+
if (config.copilotInstructions !== false) {
|
|
51
|
+
dirs.push(join(projectDir, ".github"));
|
|
52
|
+
}
|
|
53
|
+
for (const dir of dirs) {
|
|
54
|
+
if (!existsSync(dir)) {
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const files = [
|
|
59
|
+
{ target: AGENTS_MD, template: "AGENTS.md" },
|
|
60
|
+
{ target: MEMORY_FILE, template: "MEMORY.md" },
|
|
61
|
+
{ target: TASKS_FILE, template: "TASKS.md" }
|
|
62
|
+
];
|
|
63
|
+
if (config.copilotInstructions !== false) {
|
|
64
|
+
files.push({
|
|
65
|
+
target: COPILOT_INSTRUCTIONS,
|
|
66
|
+
template: "copilot-instructions.md"
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
for (const { target, template } of files) {
|
|
70
|
+
const targetPath = join(projectDir, target);
|
|
71
|
+
if (existsSync(targetPath) && !force) {
|
|
72
|
+
result.skipped.push(target);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const content = applyVars(readTemplate(template), vars);
|
|
76
|
+
writeFileSync(targetPath, content);
|
|
77
|
+
result.created.push(target);
|
|
78
|
+
}
|
|
79
|
+
const specKeep = join(projectDir, SPEC_DIR, ".gitkeep");
|
|
80
|
+
if (!existsSync(specKeep)) {
|
|
81
|
+
writeFileSync(specKeep, "");
|
|
82
|
+
result.created.push(`${SPEC_DIR}/.gitkeep`);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function guessProjectName(dir) {
|
|
87
|
+
const pkgPath = join(dir, "package.json");
|
|
88
|
+
if (existsSync(pkgPath)) {
|
|
89
|
+
try {
|
|
90
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
91
|
+
if (pkg.name) return pkg.name;
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return dir.split("/").pop() || "Project";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/opencode/plugin.ts
|
|
99
|
+
var MemoryLifecyclePlugin = async ({ client, directory }) => {
|
|
100
|
+
const memoryFile = resolve2(directory, MEMORY_FILE);
|
|
101
|
+
const tasksFile = resolve2(directory, TASKS_FILE);
|
|
102
|
+
const agentsFile = resolve2(directory, AGENTS_MD);
|
|
103
|
+
let editCount = 0;
|
|
104
|
+
let specFilesEdited = [];
|
|
105
|
+
return {
|
|
106
|
+
event: async ({ event }) => {
|
|
107
|
+
if (event.type === "session.created") {
|
|
108
|
+
const hasAgentsMd = existsSync2(agentsFile);
|
|
109
|
+
const hasMemory = existsSync2(memoryFile);
|
|
110
|
+
if (!hasMemory && hasAgentsMd) {
|
|
111
|
+
try {
|
|
112
|
+
const result = scaffold(directory, { copilotInstructions: false });
|
|
113
|
+
if (result.created.length > 0) {
|
|
114
|
+
await client.tui.showToast({
|
|
115
|
+
body: {
|
|
116
|
+
message: `Agent memory initialized: ${result.created.join(", ")}`,
|
|
117
|
+
variant: "success"
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
await client.app.log({
|
|
123
|
+
body: {
|
|
124
|
+
service: "agent-memory",
|
|
125
|
+
level: "warn",
|
|
126
|
+
message: `Auto-scaffold failed: ${err}`
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
editCount = 0;
|
|
132
|
+
specFilesEdited = [];
|
|
133
|
+
await client.app.log({
|
|
134
|
+
body: {
|
|
135
|
+
service: "agent-memory",
|
|
136
|
+
level: "info",
|
|
137
|
+
message: `Memory: ${hasMemory}, Tasks: ${existsSync2(tasksFile)}`
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (event.type === "session.idle" && editCount >= 3) {
|
|
142
|
+
if (existsSync2(tasksFile)) {
|
|
143
|
+
const content = readFileSync2(tasksFile, "utf-8");
|
|
144
|
+
const inProgressSection = content.split("## In Progress")[1];
|
|
145
|
+
if (inProgressSection?.trim()) {
|
|
146
|
+
await client.tui.showToast({
|
|
147
|
+
body: {
|
|
148
|
+
message: `${editCount} edits this session. Update .agents/TASKS.md before ending.`,
|
|
149
|
+
variant: "info"
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"tool.execute.after": async (input) => {
|
|
157
|
+
if (input.tool === "edit" || input.tool === "write") {
|
|
158
|
+
editCount++;
|
|
159
|
+
const filePath = input.args?.filePath || "";
|
|
160
|
+
if (filePath.includes(SPEC_DIR)) {
|
|
161
|
+
const shortPath = filePath.split(SPEC_DIR + "/").pop() || filePath;
|
|
162
|
+
if (!specFilesEdited.includes(shortPath)) {
|
|
163
|
+
specFilesEdited.push(shortPath);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/core/memory.ts
|
|
172
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
173
|
+
import { join as join2 } from "path";
|
|
174
|
+
function appendMemory(projectDir, entry) {
|
|
175
|
+
const filePath = join2(projectDir, MEMORY_FILE);
|
|
176
|
+
if (!existsSync3(filePath)) {
|
|
177
|
+
throw new Error(`${MEMORY_FILE} not found. Run 'agent-memory init' first.`);
|
|
178
|
+
}
|
|
179
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
180
|
+
const category = entry.category || "Decisions";
|
|
181
|
+
const line = `- ${entry.date}: ${entry.content}`;
|
|
182
|
+
const categoryHeader = `## ${category}`;
|
|
183
|
+
const headerIndex = content.indexOf(categoryHeader);
|
|
184
|
+
let updated;
|
|
185
|
+
if (headerIndex === -1) {
|
|
186
|
+
updated = content.trimEnd() + `
|
|
187
|
+
|
|
188
|
+
${categoryHeader}
|
|
189
|
+
${line}
|
|
190
|
+
`;
|
|
191
|
+
} else {
|
|
192
|
+
const afterHeader = headerIndex + categoryHeader.length;
|
|
193
|
+
const nextHeaderIndex = content.indexOf("\n## ", afterHeader);
|
|
194
|
+
const insertAt = nextHeaderIndex === -1 ? content.length : nextHeaderIndex;
|
|
195
|
+
const categoryContent = content.slice(afterHeader, insertAt);
|
|
196
|
+
const lastLineEnd = afterHeader + categoryContent.trimEnd().length;
|
|
197
|
+
updated = content.slice(0, lastLineEnd) + "\n" + line + content.slice(lastLineEnd);
|
|
198
|
+
}
|
|
199
|
+
writeFileSync2(filePath, updated);
|
|
200
|
+
}
|
|
201
|
+
function readMemory(projectDir) {
|
|
202
|
+
const filePath = join2(projectDir, MEMORY_FILE);
|
|
203
|
+
if (!existsSync3(filePath)) return {};
|
|
204
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
205
|
+
const categories = {};
|
|
206
|
+
let currentCategory = "_uncategorized";
|
|
207
|
+
for (const line of content.split("\n")) {
|
|
208
|
+
const headerMatch = line.match(/^## (.+)/);
|
|
209
|
+
if (headerMatch) {
|
|
210
|
+
currentCategory = headerMatch[1];
|
|
211
|
+
categories[currentCategory] = categories[currentCategory] || [];
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (line.startsWith("- ") && currentCategory) {
|
|
215
|
+
categories[currentCategory] = categories[currentCategory] || [];
|
|
216
|
+
categories[currentCategory].push(line.slice(2));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return categories;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/core/tasks.ts
|
|
223
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
224
|
+
import { join as join3 } from "path";
|
|
225
|
+
function readTasks(projectDir) {
|
|
226
|
+
const filePath = join3(projectDir, TASKS_FILE);
|
|
227
|
+
const result = {
|
|
228
|
+
in_progress: [],
|
|
229
|
+
up_next: [],
|
|
230
|
+
completed: []
|
|
231
|
+
};
|
|
232
|
+
if (!existsSync4(filePath)) return result;
|
|
233
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
234
|
+
let currentSection = null;
|
|
235
|
+
for (const line of content.split("\n")) {
|
|
236
|
+
if (line.startsWith("## In Progress")) {
|
|
237
|
+
currentSection = "in_progress";
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (line.startsWith("## Up Next")) {
|
|
241
|
+
currentSection = "up_next";
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (line.startsWith("## Completed")) {
|
|
245
|
+
currentSection = "completed";
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (currentSection && line.startsWith("- ")) {
|
|
249
|
+
result[currentSection].push(line.slice(2));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
function writeTasks(projectDir, tasks) {
|
|
255
|
+
const filePath = join3(projectDir, TASKS_FILE);
|
|
256
|
+
const content = `# Current Tasks
|
|
257
|
+
|
|
258
|
+
Working memory for cross-session continuity. Update before ending a session.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## In Progress
|
|
263
|
+
${tasks.in_progress.map((t) => `- ${t}`).join("\n") || ""}
|
|
264
|
+
|
|
265
|
+
## Up Next
|
|
266
|
+
${tasks.up_next.map((t) => `- ${t}`).join("\n") || ""}
|
|
267
|
+
|
|
268
|
+
## Completed
|
|
269
|
+
${tasks.completed.map((t) => `- ${t}`).join("\n") || ""}
|
|
270
|
+
`;
|
|
271
|
+
writeFileSync3(filePath, content);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/core/doctor.ts
|
|
275
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
276
|
+
import { join as join4 } from "path";
|
|
277
|
+
function doctor(projectDir) {
|
|
278
|
+
const issues = [];
|
|
279
|
+
const required = [
|
|
280
|
+
{ file: AGENTS_MD, desc: "Root router file" },
|
|
281
|
+
{ file: MEMORY_FILE, desc: "Long-term memory" },
|
|
282
|
+
{ file: TASKS_FILE, desc: "Working memory" }
|
|
283
|
+
];
|
|
284
|
+
for (const { file, desc } of required) {
|
|
285
|
+
const filePath = join4(projectDir, file);
|
|
286
|
+
if (!existsSync5(filePath)) {
|
|
287
|
+
issues.push({ level: "error", file, message: `Missing ${desc}` });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const dir of [AGENTS_DIR, SPEC_DIR]) {
|
|
291
|
+
if (!existsSync5(join4(projectDir, dir))) {
|
|
292
|
+
issues.push({ level: "error", file: dir, message: "Directory missing" });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const copilotPath = join4(projectDir, COPILOT_INSTRUCTIONS);
|
|
296
|
+
if (!existsSync5(copilotPath)) {
|
|
297
|
+
issues.push({
|
|
298
|
+
level: "warning",
|
|
299
|
+
file: COPILOT_INSTRUCTIONS,
|
|
300
|
+
message: "Copilot instructions missing \u2014 VSCode/GitHub Copilot won't have context"
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const agentsPath = join4(projectDir, AGENTS_MD);
|
|
304
|
+
if (existsSync5(agentsPath)) {
|
|
305
|
+
const content = readFileSync5(agentsPath, "utf-8");
|
|
306
|
+
if (!content.includes(".agents/")) {
|
|
307
|
+
issues.push({
|
|
308
|
+
level: "warning",
|
|
309
|
+
file: AGENTS_MD,
|
|
310
|
+
message: "No references to .agents/ \u2014 agents may not find memory files"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (content.split("\n").length > 150) {
|
|
314
|
+
issues.push({
|
|
315
|
+
level: "warning",
|
|
316
|
+
file: AGENTS_MD,
|
|
317
|
+
message: "Over 150 lines \u2014 consider keeping it concise as a router"
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const memoryPath = join4(projectDir, MEMORY_FILE);
|
|
322
|
+
if (existsSync5(memoryPath)) {
|
|
323
|
+
const lines = readFileSync5(memoryPath, "utf-8").split("\n").length;
|
|
324
|
+
if (lines > 150) {
|
|
325
|
+
issues.push({
|
|
326
|
+
level: "warning",
|
|
327
|
+
file: MEMORY_FILE,
|
|
328
|
+
message: `${lines} lines \u2014 consider compressing or archiving old entries`
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { ok: issues.filter((i) => i.level === "error").length === 0, issues };
|
|
333
|
+
}
|
|
334
|
+
export {
|
|
335
|
+
AGENTS_DIR,
|
|
336
|
+
AGENTS_MD,
|
|
337
|
+
MemoryLifecyclePlugin as AgentMemoryPlugin,
|
|
338
|
+
COPILOT_INSTRUCTIONS,
|
|
339
|
+
MEMORY_FILE,
|
|
340
|
+
SPEC_DIR,
|
|
341
|
+
TASKS_FILE,
|
|
342
|
+
appendMemory,
|
|
343
|
+
doctor,
|
|
344
|
+
readMemory,
|
|
345
|
+
readTasks,
|
|
346
|
+
scaffold,
|
|
347
|
+
writeTasks
|
|
348
|
+
};
|
package/package.json
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vuau/agent-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Structured AI memory for codebases — OpenCode plugin + scaffolding CLI",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "index.
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./index.
|
|
8
|
+
".": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
|
-
"agent-memory": "
|
|
11
|
+
"agent-memory": "dist/bin/cli.js"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
|
-
"
|
|
15
|
-
"src/",
|
|
14
|
+
"dist/",
|
|
16
15
|
"templates/",
|
|
17
|
-
"bin/",
|
|
18
16
|
"docs/",
|
|
19
17
|
"README.md"
|
|
20
18
|
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.0.0",
|
|
25
|
+
"typescript": "^6.0.3"
|
|
26
|
+
},
|
|
21
27
|
"keywords": [
|
|
22
28
|
"opencode",
|
|
23
29
|
"opencode-plugin",
|
|
@@ -32,7 +38,7 @@
|
|
|
32
38
|
"license": "MIT",
|
|
33
39
|
"repository": {
|
|
34
40
|
"type": "git",
|
|
35
|
-
"url": "https://github.com/vuau/agent-memory"
|
|
41
|
+
"url": "git+https://github.com/vuau/agent-memory.git"
|
|
36
42
|
},
|
|
37
43
|
"peerDependencies": {
|
|
38
44
|
"@opencode-ai/plugin": ">=1.0.0"
|
package/bin/cli.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node --experimental-strip-types --no-warnings
|
|
2
|
-
/**
|
|
3
|
-
* CLI entry point for @vuau/agent-memory
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* npx @vuau/agent-memory init [--force] [--name <project-name>]
|
|
7
|
-
* npx @vuau/agent-memory doctor
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { scaffold } from "../src/core/scaffold.ts"
|
|
11
|
-
import { doctor } from "../src/core/doctor.ts"
|
|
12
|
-
|
|
13
|
-
const args = process.argv.slice(2)
|
|
14
|
-
const command = args[0]
|
|
15
|
-
|
|
16
|
-
function printUsage() {
|
|
17
|
-
console.log(`
|
|
18
|
-
@vuau/agent-memory — Structured AI memory for codebases
|
|
19
|
-
|
|
20
|
-
Usage:
|
|
21
|
-
agent-memory init [options] Scaffold .agents/ structure
|
|
22
|
-
agent-memory doctor Validate .agents/ structure
|
|
23
|
-
agent-memory help Show this help
|
|
24
|
-
|
|
25
|
-
Options (init):
|
|
26
|
-
--force Overwrite existing files
|
|
27
|
-
--name <name> Project name (default: from package.json)
|
|
28
|
-
--no-copilot Skip .github/copilot-instructions.md
|
|
29
|
-
`)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function runInit() {
|
|
33
|
-
const force = args.includes("--force")
|
|
34
|
-
const noCopilot = args.includes("--no-copilot")
|
|
35
|
-
const nameIdx = args.indexOf("--name")
|
|
36
|
-
const projectName = nameIdx !== -1 ? args[nameIdx + 1] : undefined
|
|
37
|
-
|
|
38
|
-
const cwd = process.cwd()
|
|
39
|
-
console.log(`Initializing agent memory in ${cwd}...`)
|
|
40
|
-
|
|
41
|
-
const result = scaffold(cwd, {
|
|
42
|
-
projectName,
|
|
43
|
-
copilotInstructions: !noCopilot,
|
|
44
|
-
}, force)
|
|
45
|
-
|
|
46
|
-
if (result.created.length > 0) {
|
|
47
|
-
console.log("\nCreated:")
|
|
48
|
-
for (const f of result.created) {
|
|
49
|
-
console.log(` ✓ ${f}`)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (result.skipped.length > 0) {
|
|
54
|
-
console.log("\nSkipped (already exist):")
|
|
55
|
-
for (const f of result.skipped) {
|
|
56
|
-
console.log(` - ${f}`)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (result.created.length === 0 && result.skipped.length > 0) {
|
|
61
|
-
console.log("\nAll files already exist. Use --force to overwrite.")
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
console.log("\nNext steps:")
|
|
65
|
-
console.log(" 1. Edit AGENTS.md — add your project-specific rules")
|
|
66
|
-
console.log(" 2. Add spec files to .agents/spec/ for detailed documentation")
|
|
67
|
-
console.log(" 3. For OpenCode: add to opencode.json → { \"plugin\": [\"@vuau/agent-memory\"] }")
|
|
68
|
-
console.log("")
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function runDoctor() {
|
|
72
|
-
const cwd = process.cwd()
|
|
73
|
-
const result = doctor(cwd)
|
|
74
|
-
|
|
75
|
-
if (result.issues.length === 0) {
|
|
76
|
-
console.log("✓ All checks passed!")
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
for (const issue of result.issues) {
|
|
81
|
-
const icon = issue.level === "error" ? "✗" : issue.level === "warning" ? "⚠" : "ℹ"
|
|
82
|
-
console.log(` ${icon} [${issue.level}] ${issue.file}: ${issue.message}`)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
console.log("")
|
|
86
|
-
if (result.ok) {
|
|
87
|
-
console.log("⚠ Passed with warnings. Run 'agent-memory init' to fix missing files.")
|
|
88
|
-
} else {
|
|
89
|
-
console.log("✗ Failed. Run 'agent-memory init' to create missing files.")
|
|
90
|
-
process.exit(1)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
switch (command) {
|
|
95
|
-
case "init":
|
|
96
|
-
runInit()
|
|
97
|
-
break
|
|
98
|
-
case "doctor":
|
|
99
|
-
runDoctor()
|
|
100
|
-
break
|
|
101
|
-
case "help":
|
|
102
|
-
case "--help":
|
|
103
|
-
case "-h":
|
|
104
|
-
case undefined:
|
|
105
|
-
printUsage()
|
|
106
|
-
break
|
|
107
|
-
default:
|
|
108
|
-
console.error(`Unknown command: ${command}`)
|
|
109
|
-
printUsage()
|
|
110
|
-
process.exit(1)
|
|
111
|
-
}
|
package/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vuau/agent-memory
|
|
3
|
-
*
|
|
4
|
-
* Structured AI memory for codebases.
|
|
5
|
-
* OpenCode plugin entry point.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export { MemoryLifecyclePlugin as AgentMemoryPlugin } from "./src/opencode/plugin.ts"
|
|
9
|
-
|
|
10
|
-
// Re-export core for programmatic use
|
|
11
|
-
export * from "./src/core/index.ts"
|
package/src/core/doctor.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Doctor — validate .agents/ structure integrity.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { existsSync, readFileSync } from "fs"
|
|
6
|
-
import { join } from "path"
|
|
7
|
-
import {
|
|
8
|
-
AGENTS_DIR,
|
|
9
|
-
SPEC_DIR,
|
|
10
|
-
MEMORY_FILE,
|
|
11
|
-
TASKS_FILE,
|
|
12
|
-
AGENTS_MD,
|
|
13
|
-
COPILOT_INSTRUCTIONS,
|
|
14
|
-
type DoctorResult,
|
|
15
|
-
type DoctorIssue,
|
|
16
|
-
} from "./types.ts"
|
|
17
|
-
|
|
18
|
-
export function doctor(projectDir: string): DoctorResult {
|
|
19
|
-
const issues: DoctorIssue[] = []
|
|
20
|
-
|
|
21
|
-
// Check required files
|
|
22
|
-
const required = [
|
|
23
|
-
{ file: AGENTS_MD, desc: "Root router file" },
|
|
24
|
-
{ file: MEMORY_FILE, desc: "Long-term memory" },
|
|
25
|
-
{ file: TASKS_FILE, desc: "Working memory" },
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
for (const { file, desc } of required) {
|
|
29
|
-
const filePath = join(projectDir, file)
|
|
30
|
-
if (!existsSync(filePath)) {
|
|
31
|
-
issues.push({ level: "error", file, message: `Missing ${desc}` })
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Check directories
|
|
36
|
-
for (const dir of [AGENTS_DIR, SPEC_DIR]) {
|
|
37
|
-
if (!existsSync(join(projectDir, dir))) {
|
|
38
|
-
issues.push({ level: "error", file: dir, message: "Directory missing" })
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Check optional files
|
|
43
|
-
const copilotPath = join(projectDir, COPILOT_INSTRUCTIONS)
|
|
44
|
-
if (!existsSync(copilotPath)) {
|
|
45
|
-
issues.push({
|
|
46
|
-
level: "warning",
|
|
47
|
-
file: COPILOT_INSTRUCTIONS,
|
|
48
|
-
message: "Copilot instructions missing — VSCode/GitHub Copilot won't have context",
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Validate AGENTS.md has documentation map
|
|
53
|
-
const agentsPath = join(projectDir, AGENTS_MD)
|
|
54
|
-
if (existsSync(agentsPath)) {
|
|
55
|
-
const content = readFileSync(agentsPath, "utf-8")
|
|
56
|
-
if (!content.includes(".agents/")) {
|
|
57
|
-
issues.push({
|
|
58
|
-
level: "warning",
|
|
59
|
-
file: AGENTS_MD,
|
|
60
|
-
message: "No references to .agents/ — agents may not find memory files",
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
if (content.split("\n").length > 150) {
|
|
64
|
-
issues.push({
|
|
65
|
-
level: "warning",
|
|
66
|
-
file: AGENTS_MD,
|
|
67
|
-
message: "Over 150 lines — consider keeping it concise as a router",
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Validate MEMORY.md line count
|
|
73
|
-
const memoryPath = join(projectDir, MEMORY_FILE)
|
|
74
|
-
if (existsSync(memoryPath)) {
|
|
75
|
-
const lines = readFileSync(memoryPath, "utf-8").split("\n").length
|
|
76
|
-
if (lines > 150) {
|
|
77
|
-
issues.push({
|
|
78
|
-
level: "warning",
|
|
79
|
-
file: MEMORY_FILE,
|
|
80
|
-
message: `${lines} lines — consider compressing or archiving old entries`,
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return { ok: issues.filter((i) => i.level === "error").length === 0, issues }
|
|
86
|
-
}
|
package/src/core/index.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { scaffold, type ScaffoldResult } from "./scaffold.ts"
|
|
2
|
-
export { appendMemory, readMemory, type MemoryEntry } from "./memory.ts"
|
|
3
|
-
export { readTasks, writeTasks, type TaskItem, type TaskStatus } from "./tasks.ts"
|
|
4
|
-
export { doctor } from "./doctor.ts"
|
|
5
|
-
export * from "./types.ts"
|
package/src/core/memory.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory — read/append operations for MEMORY.md
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync } from "fs"
|
|
6
|
-
import { join } from "path"
|
|
7
|
-
import { MEMORY_FILE } from "./types.ts"
|
|
8
|
-
|
|
9
|
-
export interface MemoryEntry {
|
|
10
|
-
date: string
|
|
11
|
-
content: string
|
|
12
|
-
category?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Append a 1-line entry to MEMORY.md under a category.
|
|
17
|
-
* Creates category header if it doesn't exist.
|
|
18
|
-
*/
|
|
19
|
-
export function appendMemory(
|
|
20
|
-
projectDir: string,
|
|
21
|
-
entry: MemoryEntry
|
|
22
|
-
): void {
|
|
23
|
-
const filePath = join(projectDir, MEMORY_FILE)
|
|
24
|
-
if (!existsSync(filePath)) {
|
|
25
|
-
throw new Error(`${MEMORY_FILE} not found. Run 'agent-memory init' first.`)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const content = readFileSync(filePath, "utf-8")
|
|
29
|
-
const category = entry.category || "Decisions"
|
|
30
|
-
const line = `- ${entry.date}: ${entry.content}`
|
|
31
|
-
|
|
32
|
-
// Find category header
|
|
33
|
-
const categoryHeader = `## ${category}`
|
|
34
|
-
const headerIndex = content.indexOf(categoryHeader)
|
|
35
|
-
|
|
36
|
-
let updated: string
|
|
37
|
-
if (headerIndex === -1) {
|
|
38
|
-
// Append new category at end
|
|
39
|
-
updated = content.trimEnd() + `\n\n${categoryHeader}\n${line}\n`
|
|
40
|
-
} else {
|
|
41
|
-
// Find next ## header or end of file
|
|
42
|
-
const afterHeader = headerIndex + categoryHeader.length
|
|
43
|
-
const nextHeaderIndex = content.indexOf("\n## ", afterHeader)
|
|
44
|
-
const insertAt = nextHeaderIndex === -1 ? content.length : nextHeaderIndex
|
|
45
|
-
|
|
46
|
-
// Find last non-empty line in category
|
|
47
|
-
const categoryContent = content.slice(afterHeader, insertAt)
|
|
48
|
-
const lastLineEnd = afterHeader + categoryContent.trimEnd().length
|
|
49
|
-
|
|
50
|
-
updated =
|
|
51
|
-
content.slice(0, lastLineEnd) +
|
|
52
|
-
"\n" + line +
|
|
53
|
-
content.slice(lastLineEnd)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
writeFileSync(filePath, updated)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Read all entries from MEMORY.md, parsed by category.
|
|
61
|
-
*/
|
|
62
|
-
export function readMemory(
|
|
63
|
-
projectDir: string
|
|
64
|
-
): Record<string, string[]> {
|
|
65
|
-
const filePath = join(projectDir, MEMORY_FILE)
|
|
66
|
-
if (!existsSync(filePath)) return {}
|
|
67
|
-
|
|
68
|
-
const content = readFileSync(filePath, "utf-8")
|
|
69
|
-
const categories: Record<string, string[]> = {}
|
|
70
|
-
let currentCategory = "_uncategorized"
|
|
71
|
-
|
|
72
|
-
for (const line of content.split("\n")) {
|
|
73
|
-
const headerMatch = line.match(/^## (.+)/)
|
|
74
|
-
if (headerMatch) {
|
|
75
|
-
currentCategory = headerMatch[1]
|
|
76
|
-
categories[currentCategory] = categories[currentCategory] || []
|
|
77
|
-
continue
|
|
78
|
-
}
|
|
79
|
-
if (line.startsWith("- ") && currentCategory) {
|
|
80
|
-
categories[currentCategory] = categories[currentCategory] || []
|
|
81
|
-
categories[currentCategory].push(line.slice(2))
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return categories
|
|
86
|
-
}
|
package/src/core/scaffold.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scaffold — create .agents/ directory structure in a project.
|
|
3
|
-
*
|
|
4
|
-
* Idempotent: skips existing files unless force=true.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs"
|
|
8
|
-
import { join, resolve, dirname } from "path"
|
|
9
|
-
import { fileURLToPath } from "url"
|
|
10
|
-
import {
|
|
11
|
-
AGENTS_DIR,
|
|
12
|
-
SPEC_DIR,
|
|
13
|
-
MEMORY_FILE,
|
|
14
|
-
TASKS_FILE,
|
|
15
|
-
AGENTS_MD,
|
|
16
|
-
COPILOT_INSTRUCTIONS,
|
|
17
|
-
type AgentMemoryConfig,
|
|
18
|
-
} from "./types.ts"
|
|
19
|
-
|
|
20
|
-
// Resolve templates dir relative to this file (works in both Bun and Node)
|
|
21
|
-
function getTemplatesDir(): string {
|
|
22
|
-
// Try import.meta based resolution
|
|
23
|
-
try {
|
|
24
|
-
const thisDir = dirname(fileURLToPath(import.meta.url))
|
|
25
|
-
const candidate = resolve(thisDir, "../../templates")
|
|
26
|
-
if (existsSync(candidate)) return candidate
|
|
27
|
-
} catch {}
|
|
28
|
-
// Fallback: walk up from __dirname if available
|
|
29
|
-
const candidate2 = resolve(__dirname, "../../templates")
|
|
30
|
-
if (existsSync(candidate2)) return candidate2
|
|
31
|
-
throw new Error("Cannot locate templates directory")
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const TEMPLATES_DIR = getTemplatesDir()
|
|
35
|
-
|
|
36
|
-
export interface ScaffoldResult {
|
|
37
|
-
created: string[]
|
|
38
|
-
skipped: string[]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function readTemplate(name: string): string {
|
|
42
|
-
const templatePath = join(TEMPLATES_DIR, name)
|
|
43
|
-
return readFileSync(templatePath, "utf-8")
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function applyVars(content: string, vars: Record<string, string>): string {
|
|
47
|
-
let result = content
|
|
48
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
49
|
-
result = result.replaceAll(`{{${key}}}`, value)
|
|
50
|
-
}
|
|
51
|
-
return result
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function scaffold(
|
|
55
|
-
projectDir: string,
|
|
56
|
-
config: AgentMemoryConfig = {},
|
|
57
|
-
force = false
|
|
58
|
-
): ScaffoldResult {
|
|
59
|
-
const result: ScaffoldResult = { created: [], skipped: [] }
|
|
60
|
-
const projectName = config.projectName || guessProjectName(projectDir)
|
|
61
|
-
const vars = { PROJECT_NAME: projectName }
|
|
62
|
-
|
|
63
|
-
// Ensure directories exist
|
|
64
|
-
const dirs = [
|
|
65
|
-
join(projectDir, AGENTS_DIR),
|
|
66
|
-
join(projectDir, SPEC_DIR),
|
|
67
|
-
]
|
|
68
|
-
if (config.copilotInstructions !== false) {
|
|
69
|
-
dirs.push(join(projectDir, ".github"))
|
|
70
|
-
}
|
|
71
|
-
for (const dir of dirs) {
|
|
72
|
-
if (!existsSync(dir)) {
|
|
73
|
-
mkdirSync(dir, { recursive: true })
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Files to scaffold
|
|
78
|
-
const files: Array<{ target: string; template: string }> = [
|
|
79
|
-
{ target: AGENTS_MD, template: "AGENTS.md" },
|
|
80
|
-
{ target: MEMORY_FILE, template: "MEMORY.md" },
|
|
81
|
-
{ target: TASKS_FILE, template: "TASKS.md" },
|
|
82
|
-
]
|
|
83
|
-
|
|
84
|
-
if (config.copilotInstructions !== false) {
|
|
85
|
-
files.push({
|
|
86
|
-
target: COPILOT_INSTRUCTIONS,
|
|
87
|
-
template: "copilot-instructions.md",
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Write files
|
|
92
|
-
for (const { target, template } of files) {
|
|
93
|
-
const targetPath = join(projectDir, target)
|
|
94
|
-
if (existsSync(targetPath) && !force) {
|
|
95
|
-
result.skipped.push(target)
|
|
96
|
-
continue
|
|
97
|
-
}
|
|
98
|
-
const content = applyVars(readTemplate(template), vars)
|
|
99
|
-
writeFileSync(targetPath, content)
|
|
100
|
-
result.created.push(target)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Create .gitkeep in spec/ if empty
|
|
104
|
-
const specKeep = join(projectDir, SPEC_DIR, ".gitkeep")
|
|
105
|
-
if (!existsSync(specKeep)) {
|
|
106
|
-
writeFileSync(specKeep, "")
|
|
107
|
-
result.created.push(`${SPEC_DIR}/.gitkeep`)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return result
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function guessProjectName(dir: string): string {
|
|
114
|
-
// Try package.json
|
|
115
|
-
const pkgPath = join(dir, "package.json")
|
|
116
|
-
if (existsSync(pkgPath)) {
|
|
117
|
-
try {
|
|
118
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
|
|
119
|
-
if (pkg.name) return pkg.name
|
|
120
|
-
} catch {}
|
|
121
|
-
}
|
|
122
|
-
// Fallback to directory name
|
|
123
|
-
return dir.split("/").pop() || "Project"
|
|
124
|
-
}
|
package/src/core/tasks.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tasks — read/update operations for TASKS.md
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync } from "fs"
|
|
6
|
-
import { join } from "path"
|
|
7
|
-
import { TASKS_FILE } from "./types.ts"
|
|
8
|
-
|
|
9
|
-
export type TaskStatus = "in_progress" | "up_next" | "completed"
|
|
10
|
-
|
|
11
|
-
export interface TaskItem {
|
|
12
|
-
content: string
|
|
13
|
-
status: TaskStatus
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Read tasks grouped by status section.
|
|
18
|
-
*/
|
|
19
|
-
export function readTasks(projectDir: string): Record<TaskStatus, string[]> {
|
|
20
|
-
const filePath = join(projectDir, TASKS_FILE)
|
|
21
|
-
const result: Record<TaskStatus, string[]> = {
|
|
22
|
-
in_progress: [],
|
|
23
|
-
up_next: [],
|
|
24
|
-
completed: [],
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!existsSync(filePath)) return result
|
|
28
|
-
|
|
29
|
-
const content = readFileSync(filePath, "utf-8")
|
|
30
|
-
let currentSection: TaskStatus | null = null
|
|
31
|
-
|
|
32
|
-
for (const line of content.split("\n")) {
|
|
33
|
-
if (line.startsWith("## In Progress")) {
|
|
34
|
-
currentSection = "in_progress"
|
|
35
|
-
continue
|
|
36
|
-
}
|
|
37
|
-
if (line.startsWith("## Up Next")) {
|
|
38
|
-
currentSection = "up_next"
|
|
39
|
-
continue
|
|
40
|
-
}
|
|
41
|
-
if (line.startsWith("## Completed")) {
|
|
42
|
-
currentSection = "completed"
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
if (currentSection && line.startsWith("- ")) {
|
|
46
|
-
result[currentSection].push(line.slice(2))
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return result
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Write tasks back to TASKS.md, preserving header structure.
|
|
55
|
-
*/
|
|
56
|
-
export function writeTasks(
|
|
57
|
-
projectDir: string,
|
|
58
|
-
tasks: Record<TaskStatus, string[]>
|
|
59
|
-
): void {
|
|
60
|
-
const filePath = join(projectDir, TASKS_FILE)
|
|
61
|
-
const content = `# Current Tasks
|
|
62
|
-
|
|
63
|
-
Working memory for cross-session continuity. Update before ending a session.
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## In Progress
|
|
68
|
-
${tasks.in_progress.map((t) => `- ${t}`).join("\n") || ""}
|
|
69
|
-
|
|
70
|
-
## Up Next
|
|
71
|
-
${tasks.up_next.map((t) => `- ${t}`).join("\n") || ""}
|
|
72
|
-
|
|
73
|
-
## Completed
|
|
74
|
-
${tasks.completed.map((t) => `- ${t}`).join("\n") || ""}
|
|
75
|
-
`
|
|
76
|
-
writeFileSync(filePath, content)
|
|
77
|
-
}
|
package/src/core/types.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core constants and types shared across all entry points.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export const AGENTS_DIR = ".agents"
|
|
6
|
-
export const SPEC_DIR = ".agents/spec"
|
|
7
|
-
export const MEMORY_FILE = ".agents/MEMORY.md"
|
|
8
|
-
export const TASKS_FILE = ".agents/TASKS.md"
|
|
9
|
-
export const AGENTS_MD = "AGENTS.md"
|
|
10
|
-
export const COPILOT_INSTRUCTIONS = ".github/copilot-instructions.md"
|
|
11
|
-
|
|
12
|
-
export interface AgentMemoryConfig {
|
|
13
|
-
/** Project name used in templates */
|
|
14
|
-
projectName?: string
|
|
15
|
-
/** Custom spec categories to scaffold */
|
|
16
|
-
specFiles?: string[]
|
|
17
|
-
/** Whether to create .github/copilot-instructions.md */
|
|
18
|
-
copilotInstructions?: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface DoctorResult {
|
|
22
|
-
ok: boolean
|
|
23
|
-
issues: DoctorIssue[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface DoctorIssue {
|
|
27
|
-
level: "error" | "warning" | "info"
|
|
28
|
-
file: string
|
|
29
|
-
message: string
|
|
30
|
-
}
|
package/src/opencode/plugin.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenCode Plugin — Memory Lifecycle
|
|
3
|
-
*
|
|
4
|
-
* Hooks into session events to manage .agents/ structure:
|
|
5
|
-
* - session.created → auto-scaffold if missing, log status
|
|
6
|
-
* - session.idle → remind to update TASKS.md
|
|
7
|
-
* - tool.execute.after → track file edits
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { Plugin } from "@opencode-ai/plugin"
|
|
11
|
-
import { existsSync, readFileSync } from "fs"
|
|
12
|
-
import { resolve } from "path"
|
|
13
|
-
import { scaffold } from "../core/scaffold.ts"
|
|
14
|
-
import { MEMORY_FILE, TASKS_FILE, AGENTS_MD, SPEC_DIR } from "../core/types.ts"
|
|
15
|
-
|
|
16
|
-
export const MemoryLifecyclePlugin: Plugin = async ({ client, directory }) => {
|
|
17
|
-
const memoryFile = resolve(directory, MEMORY_FILE)
|
|
18
|
-
const tasksFile = resolve(directory, TASKS_FILE)
|
|
19
|
-
const agentsFile = resolve(directory, AGENTS_MD)
|
|
20
|
-
|
|
21
|
-
let editCount = 0
|
|
22
|
-
let specFilesEdited: string[] = []
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
event: async ({ event }) => {
|
|
26
|
-
if (event.type === "session.created") {
|
|
27
|
-
// Auto-scaffold .agents/ if AGENTS.md exists but .agents/ doesn't
|
|
28
|
-
const hasAgentsMd = existsSync(agentsFile)
|
|
29
|
-
const hasMemory = existsSync(memoryFile)
|
|
30
|
-
|
|
31
|
-
if (!hasMemory && hasAgentsMd) {
|
|
32
|
-
// Project has AGENTS.md but no .agents/ — scaffold memory files only
|
|
33
|
-
try {
|
|
34
|
-
const result = scaffold(directory, { copilotInstructions: false })
|
|
35
|
-
if (result.created.length > 0) {
|
|
36
|
-
await client.tui.showToast({
|
|
37
|
-
body: {
|
|
38
|
-
message: `Agent memory initialized: ${result.created.join(", ")}`,
|
|
39
|
-
variant: "success",
|
|
40
|
-
},
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
} catch (err) {
|
|
44
|
-
await client.app.log({
|
|
45
|
-
body: {
|
|
46
|
-
service: "agent-memory",
|
|
47
|
-
level: "warn",
|
|
48
|
-
message: `Auto-scaffold failed: ${err}`,
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Reset session counters
|
|
55
|
-
editCount = 0
|
|
56
|
-
specFilesEdited = []
|
|
57
|
-
|
|
58
|
-
await client.app.log({
|
|
59
|
-
body: {
|
|
60
|
-
service: "agent-memory",
|
|
61
|
-
level: "info",
|
|
62
|
-
message: `Memory: ${hasMemory}, Tasks: ${existsSync(tasksFile)}`,
|
|
63
|
-
},
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Remind about TASKS.md on session idle if significant work was done
|
|
68
|
-
if (event.type === "session.idle" && editCount >= 3) {
|
|
69
|
-
if (existsSync(tasksFile)) {
|
|
70
|
-
const content = readFileSync(tasksFile, "utf-8")
|
|
71
|
-
const inProgressSection = content.split("## In Progress")[1]
|
|
72
|
-
if (inProgressSection?.trim()) {
|
|
73
|
-
await client.tui.showToast({
|
|
74
|
-
body: {
|
|
75
|
-
message: `${editCount} edits this session. Update .agents/TASKS.md before ending.`,
|
|
76
|
-
variant: "info",
|
|
77
|
-
},
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
"tool.execute.after": async (input) => {
|
|
85
|
-
if (input.tool === "edit" || input.tool === "write") {
|
|
86
|
-
editCount++
|
|
87
|
-
const filePath = (input as any).args?.filePath || ""
|
|
88
|
-
if (filePath.includes(SPEC_DIR)) {
|
|
89
|
-
const shortPath = filePath.split(SPEC_DIR + "/").pop() || filePath
|
|
90
|
-
if (!specFilesEdited.includes(shortPath)) {
|
|
91
|
-
specFilesEdited.push(shortPath)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
}
|