@vuau/agent-memory 0.1.0
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/LICENSE +21 -0
- package/README.md +156 -0
- package/README.vi.md +145 -0
- package/bin/cli.ts +111 -0
- package/docs/ARCHITECTURE.md +285 -0
- package/docs/ARCHITECTURE.vi.md +285 -0
- package/docs/RESEARCH.md +216 -0
- package/docs/RESEARCH.vi.md +216 -0
- package/index.ts +11 -0
- package/package.json +45 -0
- package/src/core/doctor.ts +86 -0
- package/src/core/index.ts +5 -0
- package/src/core/memory.ts +86 -0
- package/src/core/scaffold.ts +124 -0
- package/src/core/tasks.ts +77 -0
- package/src/core/types.ts +30 -0
- package/src/opencode/plugin.ts +97 -0
- package/templates/AGENTS.md +44 -0
- package/templates/MEMORY.md +17 -0
- package/templates/TASKS.md +14 -0
- package/templates/copilot-instructions.md +30 -0
- package/templates/spec/.gitkeep +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} - AGENTS
|
|
2
|
+
|
|
3
|
+
Router file for AI agents. Keep under 150 lines.
|
|
4
|
+
|
|
5
|
+
## Priority
|
|
6
|
+
1. User request first.
|
|
7
|
+
2. This `AGENTS.md`.
|
|
8
|
+
3. Spec files in `.agents/spec/`.
|
|
9
|
+
4. If conflict remains, choose smallest safe change and state assumption.
|
|
10
|
+
|
|
11
|
+
## Documentation Map
|
|
12
|
+
|
|
13
|
+
| Task | Spec File |
|
|
14
|
+
|------|-----------|
|
|
15
|
+
| Past decisions & patterns | `.agents/MEMORY.md` |
|
|
16
|
+
| Current work in progress | `.agents/TASKS.md` |
|
|
17
|
+
|
|
18
|
+
> Add your own spec files to `.agents/spec/` and reference them here.
|
|
19
|
+
|
|
20
|
+
## Memory Protocol
|
|
21
|
+
|
|
22
|
+
### When to write
|
|
23
|
+
- User approves a decision or pattern → append to `.agents/MEMORY.md`
|
|
24
|
+
- Explore codebase/architecture → update relevant `.agents/spec/*.md`
|
|
25
|
+
- Start/finish a large task → update `.agents/TASKS.md`
|
|
26
|
+
|
|
27
|
+
### MEMORY.md entry format
|
|
28
|
+
```
|
|
29
|
+
- YYYY-MM-DD: <1-line decision or pattern>
|
|
30
|
+
```
|
|
31
|
+
Place under the appropriate category. Add `→ spec file` pointer if details belong in a spec.
|
|
32
|
+
|
|
33
|
+
### TASKS.md update
|
|
34
|
+
Before ending a session with unfinished work, move items to `## In Progress` or `## Up Next`.
|
|
35
|
+
|
|
36
|
+
### Rules
|
|
37
|
+
- Keep MEMORY.md entries to 1 line each. Details go in spec files.
|
|
38
|
+
- If MEMORY.md > 150 lines, archive old entries.
|
|
39
|
+
- Do not create additional memory files outside `.agents/`.
|
|
40
|
+
|
|
41
|
+
## Response Style
|
|
42
|
+
- Concise, concrete, implementation-focused.
|
|
43
|
+
- If uncertain, say `I don't know`, then give fastest verification step.
|
|
44
|
+
- Do not invent files, APIs, or command outputs.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Project Memory
|
|
2
|
+
|
|
3
|
+
Curated decisions, patterns, and pointers. Agents: read before implementing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
<!-- Add pointers to spec files: → Full spec: `.agents/spec/architecture.md` -->
|
|
9
|
+
|
|
10
|
+
## Decisions
|
|
11
|
+
<!-- 1-line entries: - YYYY-MM-DD: <decision> -->
|
|
12
|
+
|
|
13
|
+
## Patterns
|
|
14
|
+
<!-- Reusable code patterns discovered during development -->
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
<!-- Development workflow decisions and tooling choices -->
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} - Copilot Instructions (VSCode)
|
|
2
|
+
|
|
3
|
+
Router file for GitHub Copilot. Points to shared agent documentation.
|
|
4
|
+
|
|
5
|
+
## Priority
|
|
6
|
+
1. Follow direct user request.
|
|
7
|
+
2. Follow this file.
|
|
8
|
+
3. Follow spec files in `.agents/spec/`.
|
|
9
|
+
4. If conflict remains, choose smallest safe change and state assumptions.
|
|
10
|
+
|
|
11
|
+
## Documentation Map
|
|
12
|
+
|
|
13
|
+
| Task | Spec File |
|
|
14
|
+
|------|-----------|
|
|
15
|
+
| Past decisions & patterns | `.agents/MEMORY.md` |
|
|
16
|
+
| Current work in progress | `.agents/TASKS.md` |
|
|
17
|
+
|
|
18
|
+
> Add your own spec files to `.agents/spec/` and reference them here.
|
|
19
|
+
|
|
20
|
+
## Response Style
|
|
21
|
+
- Concise, concrete, implementation-focused.
|
|
22
|
+
- If uncertain, say "I don't know" and give fastest verification step.
|
|
23
|
+
- Do not invent files, APIs, or command results.
|
|
24
|
+
- Keep diffs minimal, aligned with existing conventions.
|
|
25
|
+
|
|
26
|
+
## Memory Protocol
|
|
27
|
+
- When user approves a decision → append 1-line entry to `.agents/MEMORY.md` under appropriate category.
|
|
28
|
+
- Format: `- YYYY-MM-DD: <decision>`.
|
|
29
|
+
- Read `.agents/MEMORY.md` before implementing; follow spec file pointers for details.
|
|
30
|
+
- Do not create additional memory files.
|
|
File without changes
|