Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Long-term facts, daily logs, and a scratchpad checklist stored as plain markdown

```bash
# Install the CLI globally
npm install -g agent-memory
npm install -g myagentmemory

# Or build from source
bun run build:cli
Expand All @@ -21,11 +21,14 @@ agent-memory init

# Install skill files for Claude Code and Codex
bash scripts/install-skills.sh
pwsh -File scripts/install-skills.ps1
```

This installs:
- `~/.claude/skills/agent-memory/SKILL.md` — Claude Code skill
- `~/.codex/skills/agent-memory/SKILL.md` — Codex skill
- `%USERPROFILE%\.claude\skills\agent-memory\SKILL.md` — Claude Code skill (Windows)
- `%USERPROFILE%\.codex\skills\agent-memory\SKILL.md` — Codex skill (Windows)

### Optional: Enable search with qmd

Expand Down Expand Up @@ -175,6 +178,7 @@ agent-memory write --target long_term --content "test" && agent-memory read --ta

# Install skills
bash scripts/install-skills.sh
pwsh -File scripts/install-skills.ps1
```

## Publishing (maintainers)
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
{
"name": "agent-memory",
"version": "0.4.0",
"name": "myagentmemory",
"version": "0.4.3",
"description": "Persistent memory for coding agents (Claude Code, Codex) with qmd-powered semantic search across daily logs, long-term memory, and scratchpad",
"main": "src/core.ts",
"main": "./dist/core.js",
"types": "./dist/core.d.ts",
"exports": {
".": {
"types": "./dist/core.d.ts",
"default": "./dist/core.js"
}
},
"bin": {
"agent-memory": "./dist/agent-memory"
},
Expand Down Expand Up @@ -42,7 +49,9 @@
"scripts": {
"postinstall": "node scripts/postinstall.cjs",
"build": "tsc -p tsconfig.json --noEmit",
"build:lib": "tsc -p tsconfig.build.json",
"build:cli": "bun build src/cli.ts --compile --outfile dist/agent-memory",
"prepack": "npm run build:lib && bun run build:cli",
"lint": "biome check .",
"test": "bun test test/unit.test.ts",
"test:unit": "bun test test/unit.test.ts",
Expand Down
48 changes: 48 additions & 0 deletions scripts/install-skills.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
$ErrorActionPreference = "Stop"

$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectDir = Split-Path -Parent $ScriptDir

# Claude Code skill
$ClaudeSkillDir = Join-Path $env:USERPROFILE ".claude\\skills\\agent-memory"
$ClaudeSkillSrc = Join-Path $ProjectDir "skills\\claude-code\\SKILL.md"
if (Test-Path $ClaudeSkillSrc) {
New-Item -ItemType Directory -Force -Path $ClaudeSkillDir | Out-Null
Copy-Item -Force $ClaudeSkillSrc (Join-Path $ClaudeSkillDir "SKILL.md")
Write-Host "Installed Claude Code skill: $ClaudeSkillDir\\SKILL.md"
} else {
Write-Host "Skipping Claude Code skill (skills\\claude-code\\SKILL.md not found)"
}

# Codex skill
$CodexSkillDir = Join-Path $env:USERPROFILE ".codex\\skills\\agent-memory"
$CodexSkillSrc = Join-Path $ProjectDir "skills\\codex\\SKILL.md"
if (Test-Path $CodexSkillSrc) {
New-Item -ItemType Directory -Force -Path $CodexSkillDir | Out-Null
Copy-Item -Force $CodexSkillSrc (Join-Path $CodexSkillDir "SKILL.md")
Write-Host "Installed Codex skill: $CodexSkillDir\\SKILL.md"
} else {
Write-Host "Skipping Codex skill (skills\\codex\\SKILL.md not found)"
}

Write-Host ""
Write-Host "Done."
Write-Host ""

$BinExe = Join-Path $ProjectDir "dist\\agent-memory.exe"
$Bin = Join-Path $ProjectDir "dist\\agent-memory"
if (Test-Path $BinExe) {
Write-Host "Add this directory to your PATH:"
Write-Host " $(Join-Path $ProjectDir 'dist')"
} elseif (Test-Path $Bin) {
Write-Host "Add this directory to your PATH:"
Write-Host " $(Join-Path $ProjectDir 'dist')"
} else {
Write-Host "Build the CLI binary first:"
Write-Host " bun run build:cli"
Write-Host "Then add this directory to your PATH:"
Write-Host " $(Join-Path $ProjectDir 'dist')"
}

Write-Host ""
Write-Host "Initialize memory: agent-memory init"
25 changes: 23 additions & 2 deletions scripts/install-skills.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,28 @@ else
fi

echo ""
echo "Done. Make sure 'agent-memory' is on your PATH:"
echo " npm install -g agent-memory # or: bun run build:cli && ln -s dist/agent-memory /usr/local/bin/"
echo "Done."
echo ""
AGENT_MEMORY_BIN="$PROJECT_DIR/dist/agent-memory"
if [ -x "$AGENT_MEMORY_BIN" ]; then
if [ -d "$HOME/.local/bin" ]; then
if ln -sf "$AGENT_MEMORY_BIN" "$HOME/.local/bin/agent-memory" 2>/dev/null; then
echo "Linked agent-memory into: $HOME/.local/bin/agent-memory"
echo "Ensure $HOME/.local/bin is on your PATH."
else
echo "Could not link to $HOME/.local/bin (permissions)."
echo "Add this to your PATH instead:"
echo " $AGENT_MEMORY_BIN"
fi
else
echo "Make sure 'agent-memory' is on your PATH:"
echo " $AGENT_MEMORY_BIN"
fi
else
echo "Build the CLI binary first:"
echo " bun run build:cli"
echo "Then add it to your PATH:"
echo " $AGENT_MEMORY_BIN"
fi
echo ""
echo "Initialize memory: agent-memory init"
117 changes: 47 additions & 70 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,7 @@ async function cmdRead(flags: Record<string, string | boolean>) {
if (target === "scratchpad") {
const content = readFileSafe(getScratchpadFile());
if (!content?.trim()) {
output(
json
? { content: null }
: "SCRATCHPAD.md is empty or does not exist.",
json,
);
output(json ? { content: null } : "SCRATCHPAD.md is empty or does not exist.", json);
return;
}
output(json ? { content, path: getScratchpadFile() } : content, json);
Expand All @@ -253,28 +248,19 @@ async function cmdRead(flags: Record<string, string | boolean>) {
// long_term
const content = readFileSafe(getMemoryFile());
if (!content) {
output(
json ? { content: null } : "MEMORY.md is empty or does not exist.",
json,
);
output(json ? { content: null } : "MEMORY.md is empty or does not exist.", json);
return;
}
output(json ? { content, path: getMemoryFile() } : content, json);
}

async function cmdScratchpad(
flags: Record<string, string | boolean>,
positional: string[],
) {
async function cmdScratchpad(flags: Record<string, string | boolean>, positional: string[]) {
const json = hasFlag(flags, "json");
const action = positional[0];
const text = getFlag(flags, "text");

if (!action || !["add", "done", "undo", "clear_done", "list"].includes(action)) {
exitError(
"Usage: agent-memory scratchpad <add|done|undo|clear_done|list> [--text <text>]",
json,
);
exitError("Usage: agent-memory scratchpad <add|done|undo|clear_done|list> [--text <text>]", json);
}

ensureDirs();
Expand All @@ -288,11 +274,14 @@ async function cmdScratchpad(
return;
}
if (json) {
output({
items: items.map((i) => ({ done: i.done, text: i.text })),
count: items.length,
open: items.filter((i) => !i.done).length,
}, true);
output(
{
items: items.map((i) => ({ done: i.done, text: i.text })),
count: items.length,
open: items.filter((i) => !i.done).length,
},
true,
);
} else {
console.log(serializeScratchpad(items));
}
Expand Down Expand Up @@ -323,10 +312,7 @@ async function cmdScratchpad(
}
}
if (!matched) {
exitError(
`No matching ${targetDone ? "open" : "done"} item found for: "${text}"`,
json,
);
exitError(`No matching ${targetDone ? "open" : "done"} item found for: "${text}"`, json);
}
fs.writeFileSync(spFile, serializeScratchpad(items), "utf-8");
await ensureQmdAvailableForUpdate();
Expand All @@ -342,10 +328,7 @@ async function cmdScratchpad(
fs.writeFileSync(spFile, serializeScratchpad(items), "utf-8");
await ensureQmdAvailableForUpdate();
scheduleQmdUpdate();
output(
json ? { ok: true, action, removed } : `Cleared ${removed} done item(s).`,
json,
);
output(json ? { ok: true, action, removed } : `Cleared ${removed} done item(s).`, json);
}
}

Expand All @@ -368,10 +351,7 @@ async function cmdSearch(flags: Record<string, string | boolean>) {
const collName = getCollectionName();
const hasCollection = await checkCollection(collName);
if (!hasCollection) {
exitError(
`qmd collection '${collName}' not found. Run: agent-memory init`,
json,
);
exitError(`qmd collection '${collName}' not found. Run: agent-memory init`, json);
}

try {
Expand All @@ -385,9 +365,7 @@ async function cmdSearch(flags: Record<string, string | boolean>) {
if (results.length === 0) {
const needsEmbed = /need embeddings/i.test(stderr ?? "");
if (needsEmbed && (mode === "semantic" || mode === "deep")) {
console.log(
`No results found. qmd reports missing embeddings — run: qmd embed`,
);
console.log(`No results found. qmd reports missing embeddings — run: qmd embed`);
} else {
console.log(`No results found for "${query}" (mode: ${mode}).`);
}
Expand All @@ -405,10 +383,7 @@ async function cmdSearch(flags: Record<string, string | boolean>) {
console.log("");
}
} catch (err) {
exitError(
`Search failed: ${err instanceof Error ? err.message : String(err)}`,
json,
);
exitError(`Search failed: ${err instanceof Error ? err.message : String(err)}`, json);
}
}

Expand All @@ -430,12 +405,15 @@ async function cmdInit(flags: Record<string, string | boolean>) {
}

if (json) {
output({
ok: true,
directory: dir,
qmd: qmdFound,
collectionCreated,
}, true);
output(
{
ok: true,
directory: dir,
qmd: qmdFound,
collectionCreated,
},
true,
);
} else {
console.log(`Memory directory: ${dir}`);
console.log(` MEMORY.md, SCRATCHPAD.md, daily/ created.`);
Expand Down Expand Up @@ -466,9 +444,7 @@ async function cmdStatus(flags: Record<string, string | boolean>) {

let dailyCount = 0;
try {
dailyCount = fs
.readdirSync(dailyDir)
.filter((f) => f.endsWith(".md")).length;
dailyCount = fs.readdirSync(dailyDir).filter((f) => f.endsWith(".md")).length;
} catch {
// directory may not exist
}
Expand All @@ -480,26 +456,27 @@ async function cmdStatus(flags: Record<string, string | boolean>) {
}

if (json) {
output({
directory: dir,
memoryFile: {
exists: memContent !== null,
chars: memContent?.length ?? 0,
lines: memContent ? memContent.split("\n").length : 0,
},
scratchpadFile: {
exists: spContent !== null,
items: spContent ? parseScratchpad(spContent).length : 0,
openItems: spContent
? parseScratchpad(spContent).filter((i) => !i.done).length
: 0,
},
dailyLogs: dailyCount,
qmd: {
available: qmdFound,
collection: hasCollection ? getCollectionName() : null,
output(
{
directory: dir,
memoryFile: {
exists: memContent !== null,
chars: memContent?.length ?? 0,
lines: memContent ? memContent.split("\n").length : 0,
},
scratchpadFile: {
exists: spContent !== null,
items: spContent ? parseScratchpad(spContent).length : 0,
openItems: spContent ? parseScratchpad(spContent).filter((i) => !i.done).length : 0,
},
dailyLogs: dailyCount,
qmd: {
available: qmdFound,
collection: hasCollection ? getCollectionName() : null,
},
},
}, true);
true,
);
} else {
console.log(`Memory directory: ${dir}`);
console.log("");
Expand Down
7 changes: 2 additions & 5 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import * as path from "node:path";
// Paths (mutable for testing via _setBaseDir / _resetBaseDir)
// ---------------------------------------------------------------------------

const DEFAULT_MEMORY_DIR =
process.env.AGENT_MEMORY_DIR ?? path.join(process.env.HOME ?? "~", ".agent-memory");
const DEFAULT_MEMORY_DIR = process.env.AGENT_MEMORY_DIR ?? path.join(process.env.HOME ?? "~", ".agent-memory");

let MEMORY_DIR = DEFAULT_MEMORY_DIR;
let MEMORY_FILE = path.join(MEMORY_DIR, "MEMORY.md");
Expand Down Expand Up @@ -683,9 +682,7 @@ export function runQmdSearch(
}
try {
const parsed = parseQmdJson(stdout);
const results = Array.isArray(parsed)
? parsed
: ((parsed as any).results ?? (parsed as any).hits ?? []);
const results = Array.isArray(parsed) ? parsed : ((parsed as any).results ?? (parsed as any).hits ?? []);
resolve({ results, stderr: stderr ?? "" });
} catch (parseErr) {
if (parseErr instanceof Error) {
Expand Down
Loading
Loading